现代Python中自定义异常的正确声明方式是什么?

1870 投票
17 回答
1149230 浏览
提问于 2025-04-15 13:49

在现代Python中,如何正确地声明自定义异常类呢?我的主要目标是遵循其他异常类的标准,这样的话,任何我在异常中添加的额外信息都能被捕获异常的工具打印出来。

这里说的“现代Python”是指能够在Python 2.5上运行,但又符合Python 2.6和Python 3.*的做法。而“自定义”则是指一个Exception对象,可以包含关于错误原因的额外数据:比如一个字符串,或者一些与异常相关的其他对象。

我在使用Python 2.6.2时遇到了一个弃用警告,让我有些困惑:

>>> class MyError(Exception):
...     def __init__(self, message):
...         self.message = message
... 
>>> MyError("foo")
_sandbox.py:3: DeprecationWarning: BaseException.message has been deprecated as of Python 2.6

看起来很奇怪的是,BaseException对名为message的属性有特殊的含义。我从PEP-352了解到,这个属性在2.5版本中确实有特殊的含义,但他们现在想要弃用它,所以我想这个名字(而且只有这个名字)现在是禁止使用的?真让人无奈。

我也隐约知道Exception有一个神奇的参数args,但我从来不知道怎么用它。而且我也不确定这是否是未来的正确做法;我在网上找到的很多讨论都提到他们想在Python 3中去掉这个args。

更新:有两个回答建议重写__init____str__/__unicode__/__repr__。这看起来需要写很多代码,这真的有必要吗?

17 个回答

315

“在现代Python中,声明自定义异常的正确方法是什么?”

这样做是可以的,前提是你的异常确实是某种更具体异常的类型:

class MyException(Exception):
    pass

或者更好(也许是完美的),与其用pass,不如给它加个文档字符串:

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

继承异常子类

来自文档

Exception

所有内置的、非系统退出的异常都是从这个类派生的。所有用户定义的异常也应该从这个类派生。

这意味着如果你的异常是某种更具体异常的类型,应该继承那个具体的异常,而不是通用的Exception(这样做的结果是你仍然是从Exception派生的,正如文档所推荐的)。另外,你至少可以提供一个文档字符串(而不必强制使用pass关键字):

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

用自定义的__init__设置你自己创建的属性。避免将字典作为位置参数传递,未来使用你代码的人会感谢你。如果你使用了已弃用的消息属性,自己赋值可以避免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) 

其实没必要自己写__str____repr__。内置的这些方法已经很好了,而你的协作继承确保你会使用它们。

对最佳答案的批评

也许我没理解问题,但为什么不这样做:

class MyException(Exception):
    pass

上面的问题在于,要捕获它,你要么得具体命名它(如果在其他地方创建的话,还得导入),要么捕获Exception(但你可能没准备好处理所有类型的异常,应该只捕获你准备处理的异常)。下面的批评类似,但另外一点是,这不是通过super初始化的正确方式,如果你访问消息属性,会得到DeprecationWarning

编辑:要重写某个东西(或传递额外参数),这样做:

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

这样你可以将错误消息的字典传递给第二个参数,之后通过e.errors访问它

这还要求传入正好两个参数(除了self)。不能多也不能少。这是一个有趣的限制,未来的用户可能不太喜欢。

直接说,这违反了里斯科夫替换原则

我将演示这两个错误:

>>> 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'

与之相比:

>>> MyAppValueError('foo', 'FOO', 'bar').message
'foo'
713

在现代的Python异常处理中,你不需要去乱用 .message,也不需要重写 .__str__().__repr__() 这些东西。如果你只是想在抛出异常时看到一个有用的信息,可以这样做:

class MyException(Exception):
    pass

raise MyException("My hovercraft is full of eels")

这样做会给你一个错误追踪信息,最后会显示 MyException: My hovercraft is full of eels

如果你想要更灵活的异常处理,可以把一个字典作为参数传进去:

raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})

不过,要在 except 块中获取这些详细信息就有点复杂了。那些详细信息会存储在 args 属性里,这个属性是一个列表。你需要这样做:

try:
    raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})
except MyException as e:
    details = e.args[0]
    print(details["animal"])

当然,你还是可以把多个信息传给异常,并通过元组索引来访问它们,但这不推荐(而且之前还打算不再支持这种做法)。如果你确实需要超过一条信息,而上面的方法又不够用,那么你应该像在教程中描述的那样,去创建一个 Exception 的子类。

class MyError(Exception):
    def __init__(self, message, animal):
        self.message = message
        self.animal = animal
    def __str__(self):
        return self.message
1955

也许我没理解问题,但为什么不这样做呢:

class MyException(Exception):
    pass

如果你想要覆盖某些内容(或者传递额外的参数),可以这样做:

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

这样你就可以把错误信息的字典传给第二个参数,之后可以通过 e.errors 来获取这些信息。

在Python 2中,你需要使用这种稍微复杂一点的 super() 形式:

super(ValidationError, self).__init__(message)

撰写回答