Proper way to declare custom exceptions in modern Python?

后端 未结 11 1633
栀梦
栀梦 2020-11-22 08:04

What\'s the proper way to declare custom exception classes in modern Python? My primary goal is to follow whatever standard other exception classes have, so that (for instan

相关标签:
11条回答
  • 2020-11-22 08:22

    No, "message" is not forbidden. It's just deprecated. You application will work fine with using message. But you may want to get rid of the deprecation error, of course.

    When you create custom Exception classes for your application, many of them do not subclass just from Exception, but from others, like ValueError or similar. Then you have to adapt to their usage of variables.

    And if you have many exceptions in your application it's usually a good idea to have a common custom base class for all of them, so that users of your modules can do

    try:
        ...
    except NelsonsExceptions:
        ...
    

    And in that case you can do the __init__ and __str__ needed there, so you don't have to repeat it for every exception. But simply calling the message variable something else than message does the trick.

    In any case, you only need the __init__ or __str__ if you do something different from what Exception itself does. And because if the deprecation, you then need both, or you get an error. That's not a whole lot of extra code you need per class. ;)

    0 讨论(0)
  • 2020-11-22 08:23

    see how exceptions work by default if one vs more attributes are used (tracebacks omitted):

    >>> raise Exception('bad thing happened')
    Exception: bad thing happened
    
    >>> raise Exception('bad thing happened', 'code is broken')
    Exception: ('bad thing happened', 'code is broken')
    

    so you might want to have a sort of "exception template", working as an exception itself, in a compatible way:

    >>> nastyerr = NastyError('bad thing happened')
    >>> raise nastyerr
    NastyError: bad thing happened
    
    >>> raise nastyerr()
    NastyError: bad thing happened
    
    >>> raise nastyerr('code is broken')
    NastyError: ('bad thing happened', 'code is broken')
    

    this can be done easily with this subclass

    class ExceptionTemplate(Exception):
        def __call__(self, *args):
            return self.__class__(*(self.args + args))
    # ...
    class NastyError(ExceptionTemplate): pass
    

    and if you don't like that default tuple-like representation, just add __str__ method to the ExceptionTemplate class, like:

        # ...
        def __str__(self):
            return ': '.join(self.args)
    

    and you'll have

    >>> raise nastyerr('code is broken')
    NastyError: bad thing happened: code is broken
    
    0 讨论(0)
  • 2020-11-22 08:31

    See a very good article "The definitive guide to Python exceptions". The basic principles are:

    • Always inherit from (at least) Exception.
    • Always call BaseException.__init__ with only one argument.
    • When building a library, define a base class inheriting from Exception.
    • Provide details about the error.
    • Inherit from builtin exceptions types when it makes sense.

    There is also information on organizing (in modules) and wrapping exceptions, I recommend to read the guide.

    0 讨论(0)
  • 2020-11-22 08:34

    To define your own exceptions correctly, there are a few best practices that you should follow:

    • Define a base class inheriting from Exception. This will allow to easily catch any exceptions related to the project:

      class MyProjectError(Exception):
          """A base class for MyProject exceptions."""
      

      Organizing the exception classes in a separate module (e.g. exceptions.py) is generally a good idea.

    • To create a specific exception, subclass the base exception class.

    • To add support for extra argument(s) to a custom exception, define a custom __init__() method with a variable number of arguments. Call the base class's __init__(), passing any positional arguments to it (remember that BaseException/Exception expect any number of positional arguments):

      class CustomError(MyProjectError):
          def __init__(self, *args, **kwargs):
              super().__init__(*args)
              self.foo = kwargs.get('foo')
      

      To raise such exception with an extra argument you can use:

       raise CustomError('Something bad happened', foo='foo')
      

    This design adheres to the Liskov substitution principle, since you can replace an instance of a base exception class with an instance of a derived exception class. Also, it allows you to create an instance of a derived class with the same parameters as the parent.

    0 讨论(0)
  • 2020-11-22 08:35

    "Proper way to declare custom exceptions in modern Python?"

    This is fine, unless your exception is really a type of a more specific exception:

    class MyException(Exception):
        pass
    

    Or better (maybe perfect), instead of pass give a docstring:

    class MyException(Exception):
        """Raise for my specific kind of exception"""
    

    Subclassing Exception Subclasses

    From the docs

    Exception

    All built-in, non-system-exiting exceptions are derived from this class. All user-defined exceptions should also be derived from this class.

    That means that if your exception is a type of a more specific exception, subclass that exception instead of the generic Exception (and the result will be that you still derive from Exception as the docs recommend). Also, you can at least provide a docstring (and not be forced to use the pass keyword):

    class MyAppValueError(ValueError):
        '''Raise when my specific value is wrong'''
    

    Set attributes you create yourself with a custom __init__. Avoid passing a dict as a positional argument, future users of your code will thank you. If you use the deprecated message attribute, assigning it yourself will avoid a DeprecationWarning:

    class MyAppValueError(ValueError):
        '''Raise when a specific subset of values in context of app is wrong'''
        def __init__(self, message, foo, *args):
            self.message = message # without this you may get DeprecationWarning
            # Special attribute you desire with your Error, 
            # perhaps the value that caused the error?:
            self.foo = foo         
            # allow users initialize misc. arguments as any other builtin Error
            super(MyAppValueError, self).__init__(message, foo, *args) 
    

    There's really no need to write your own __str__ or __repr__. The builtin ones are very nice, and your cooperative inheritance ensures that you use it.

    Critique of the top answer

    Maybe I missed the question, but why not:

    class MyException(Exception):
        pass
    

    Again, the problem with the above is that in order to catch it, you'll either have to name it specifically (importing it if created elsewhere) or catch Exception, (but you're probably not prepared to handle all types of Exceptions, and you should only catch exceptions you are prepared to handle). Similar criticism to the below, but additionally that's not the way to initialize via super, and you'll get a DeprecationWarning if you access the message attribute:

    Edit: to override something (or pass extra args), do this:

    class ValidationError(Exception):
        def __init__(self, message, errors):
    
            # Call the base class constructor with the parameters it needs
            super(ValidationError, self).__init__(message)
    
            # Now for your custom code...
            self.errors = errors
    

    That way you could pass dict of error messages to the second param, and get to it later with e.errors

    It also requires exactly two arguments to be passed in (aside from the self.) No more, no less. That's an interesting constraint that future users may not appreciate.

    To be direct - it violates Liskov substitutability.

    I'll demonstrate both errors:

    >>> ValidationError('foo', 'bar', 'baz').message
    
    Traceback (most recent call last):
      File "<pyshell#10>", line 1, in <module>
        ValidationError('foo', 'bar', 'baz').message
    TypeError: __init__() takes exactly 3 arguments (4 given)
    
    >>> ValidationError('foo', 'bar').message
    __main__:1: DeprecationWarning: BaseException.message has been deprecated as of Python 2.6
    'foo'
    

    Compared to:

    >>> MyAppValueError('foo', 'FOO', 'bar').message
    'foo'
    
    0 讨论(0)
提交回复
热议问题