在现代Python中声明自定义异常的正确方法?


1288

在现代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__。好像要打字很多,有必要吗?

Answers:


1321

也许我错过了这个问题,但是为什么不呢?

class MyException(Exception):
    pass

编辑:要覆盖某些内容(或传递额外的args),请执行以下操作:

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


Python 3更新:在Python 3+中,您可以使用以下更紧凑的用法super()

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

35
但是,这样定义的异常将不可选。看到这里的讨论stackoverflow.com/questions/16244923/…–
jiakai

86
@jiakai的意思是“可拾取”。:-)
Robino

1
根据python有关用户定义的异常的文档,__ init__函数中提到的名称不正确。而不是(自我,消息,错误),而是(自我,表达,消息)。属性表达式是发生错误的输入表达式,消息是对错误的解释。
ddleon

1
那是个误会,@ ddleon。您引用的文档中的示例适用于特定用例。子类的构造函数参数名称(也不包括其编号)没有意义。
asthasr

498

随着现代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

2
“但是将来会不推荐使用”-是否仍然打算弃用?Python 3.7似乎仍然很乐意接受Exception(foo, bar, qux)
mtraceur '18

自从上次尝试由于过渡之苦而失败以来,它还没有看到任何将其贬值的最新工作,但是仍然不鼓励使用。我将更新我的答案以反映这一点。
frnknstn

@frnknstn,为什么不建议这样做?看起来对我来说是个好习惯。

2
@neves首先,使用元组存储异常信息与使用字典进行相同操作没有任何好处。如果您对例外更改背后的原因感兴趣,请查看PEP352
frnknstn

PEP352的相关部分是缩进的想法”
liberforce

196

“在现代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'''

设置可使用custom创建自己的属性__init__。避免将dict作为位置参数传递,以后您的代码用户将感谢您。如果您使用不推荐使用的message属性,则自行分配该属性将避免出现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(但您可能不准备处理所有类型的Exception,并且您应该只捕获您准备处理的异常)。与以下内容类似的批评,但除此之外,这不是通过进行初始化的方式superDeprecationWarning如果您访问message属性,则会得到一个:

编辑:要覆盖某些内容(或传递额外的args),请执行以下操作:

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。除外)。这是一个有趣的约束,将来的用户可能不会欣赏。

直截了当-它违反了Liskov的可替代性

我将演示两个错误:

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

2
您好,2018年!BaseException.message在Python 3中已经不复存在了,所以批评只适用于旧版本,对吗?
科斯(Kos)'18年

5
@Kos对Liskov替代性的批评仍然有效。第一个参数作为“消息”的语义也可以说是有问题的,但是我认为我不会争论这一点。当我有更多空闲时间时,我会多看一下。
亚伦·霍尔

1
FWIW,用于Python 3(至少为3.6+),一个将重新定义__str__的方法MyAppValueError,而不是依赖于message属性
JACQUOT

1
@AaronHall您可以扩展子类化ValueError而不是Exception的好处吗?您声明这是文档的意思,但直接阅读不支持该解释,并且在Python教程的“用户定义的异常”下,它明确地使用户可以选择:“异常通常应从Exception类派生,直接或间接。” 因此,请热切了解您的观点是否合理。
ostergaard

1
@ostergaard现在无法完全回答,但总之,用户可以获得catch的附加选项ValueError。如果它属于“值错误”类别,这是有道理的。如果它不在“值错误”类别中,那么我会在语义上反对它。程序员有一些细微差别和推理的余地,但是我更喜欢在适用时的专一性。我会尽快更新答案,以更好地解决这个问题。
亚伦·霍尔

50

见异常缺省情况下是如何工作的,如果一个VS多个属性使用(回溯略):

>>> raise Exception('bad thing happened')
Exception: bad thing happened

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

因此,您可能需要一种“ 异常模板 ”,以兼容的方式作为异常本身工作:

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

使用此子类可以轻松完成此操作

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

如果您不喜欢默认的类似元组的表示形式,只需将__str__方法添加到ExceptionTemplate类中,例如:

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

然后你会

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

32

从Python 3.8(2018,https://docs.python.org/dev/whatsnew/3.8.html)开始,推荐的方法仍然是:

class CustomExceptionName(Exception):
    """Exception raised when very uncommon things happen"""
    pass

请不要忘记记录文档,为什么需要自定义异常!

如果需要,这是处理包含更多数据的异常的方法:

class CustomExceptionName(Exception):
    """Still an exception raised when uncommon things happen"""
    def __init__(self, message, payload=None):
        self.message = message
        self.payload = payload # you could add more args
    def __str__(self):
        return str(self.message) # __str__() obviously expects a string to be returned, so make sure not to send any other data types

并像这样获取它们:

try:
    raise CustomExceptionName("Very bad mistake.", "Forgot upgrading from Python 1")
except CustomExceptionName as error:
    print(str(error)) # Very bad mistake
    print("Detail: {}".format(error.payload)) # Detail: Forgot upgrading from Python 1

payload=None使它变得可腌很重要。转储之前,您必须致电error.__reduce__()。加载将按预期工作。

return如果您需要将大量数据传输到某些外部结构,则可能应该调查使用pythons 语句查找解决方案。对我来说,这似乎更清楚/更pythonic。高级异常在Java中大量使用,当使用框架并不得不捕获所有可能的错误时,有时会很烦人。


1
至少,当前文档指出这样做是这样做的方式(至少没有__str__),而不是使用super().__init__(...)..的其他答案。只是为了更好地“默认”序列化而改掉了__str____repr__可能是必要的。
kevlarr

2
诚实的问题:为什么使腌制异常很重要?转储和加载异常的用例有哪些?
Roel Schroeven '19年

1
@RoelSchroeven:我不得不并行化一次代码。可以很好地处理单个进程,但是它的某些类的某些方面不能序列化(lambda函数作为对象传递)。花了我一些时间弄清楚并修复它。这意味着以后可能有人需要将您的代码序列化,无法执行,并且必须找出原因。
logicOnAbstractions

17

您应该重写__repr____unicode__方法,而不使用消息,构造异常时提供的参数将位于args异常对象的属性中。


7

不,“消息”不是禁止的。只是过时了。您的应用程序可以正常使用消息。但是,您当然可以摆脱折旧错误。

当为应用程序创建自定义Exception类时,它们中的许多不仅仅从Exception继承子类,还从ValueError之类的其他子类继承子类。然后,您必须适应它们对变量的使用。

而且,如果您的应用程序中有很多异常,通常最好为所有异常都拥有一个通用的自定义基类,以便模块的用户可以

try:
    ...
except NelsonsExceptions:
    ...

在这种情况下,您可以在__init__ and __str__那里进行所需的操作,因此您不必为每个异常重复执行该操作。但是简单地调用message变量而不是message可以解决问题。

无论如何,__init__ or __str__如果您做的事情与Exception本身不同,则仅需要使用。并且因为如果不赞成使用,那么您将同时需要两者,否则您将得到一个错误。每个类不需要很多额外的代码。;)


有趣的是Django异常不会从通用基础继承。docs.djangoproject.com/en/2.2/_modules/django/core/exceptions当需要捕获来自特定应用程序的所有异常时,您是否有一个很好的例子?(也许它仅对某些特定类型的应用程序有用)。
Yaroslav Nikitenko

我在julien.danjou.info/python-exceptions-guide上找到了关于此主题的好文章。我认为异常应该主要基于域而不是基于应用程序进行子类化。当您的应用程序有关HTTP协议时,您将从HTTPError派生。当应用程序的一部分是TCP时,您将从TCPError派生该部分的异常。但是,如果您的应用程序跨很多域(文件,权限等),则MyBaseException的原因就会减少。还是要防止“违反层”?
Yaroslav Nikitenko

6

请参阅非常好的文章“ Python异常的权威指南 ”。基本原则是:

  • 始终从(至少)继承。
  • 始终BaseException.__init__仅使用一个参数进行调用。
  • 构建库时,请定义从Exception继承的基类。
  • 提供有关错误的详细信息。
  • 从内置异常类型继承是有意义的。

还有关于组织(在模块中)和包装异常的信息,我建议阅读指南。


1
这是一个很好的例子,说明为什么我通常会检查最受支持的答案,但也要检查最近的答案。有用的补充,谢谢。
logicOnAbstractions

1
Always call BaseException.__init__ with only one argument.似乎不需要的约束,因为它实际上接受任何数量的参数。
Eugene Yarmash

@EugeneYarmash我同意,现在我不明白。反正我不使用它。也许我应该重新阅读文章并扩大答案。
Yaroslav Nikitenko

@EugeneYarmash我再次阅读了这篇文章。据说,在有多个参数的情况下,C实现调用“ return PyObject_Str(self-> args);”。这意味着一个字符串应该比几个字符串更好。你检查了吗?
Yaroslav Nikitenko

3

试试这个例子

class InvalidInputError(Exception):
    def __init__(self, msg):
        self.msg = msg
    def __str__(self):
        return repr(self.msg)

inp = int(input("Enter a number between 1 to 10:"))
try:
    if type(inp) != int or inp not in list(range(1,11)):
        raise InvalidInputError
except InvalidInputError:
    print("Invalid input entered")

1

要正确定义自己的异常,需要遵循一些最佳实践:

  • 定义一个继承自的基类Exception。这将允许捕获与项目相关的任何异常(更具体的异常应从该项目继承):

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

    在单独的模块(例如exceptions.py)中组织这些异常类通常是一个好主意。

  • 要将额外的参数传递给您的异常,请定义一个__init__()具有可变数量参数的自定义方法。调用基类__init__()将任何位置参数传递给它(记住BaseException/Exception期望有任意数量的位置参数):

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

    要使用额外的参数引发此类异常,可以使用:

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

    这样的设计居然坚持里氏替换原则,因为你可以用一个派生的异常类的实例代替基本异常类的实例。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.