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

Nel*_*son 1176 python exception custom-exceptions

在现代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
Run Code Online (Sandbox Code Playgroud)

看起来很疯狂,BaseException对于名为的属性具有特殊含义message.我从PEP-352收集到该属性确实在2.5中有特殊含义他们试图弃用,所以我猜这个名字(而且仅此一个)现在被禁止了?啊.

我也模糊地意识到它Exception有一些神奇的参数args,但我从来不知道如何使用它.我也不确定这是向前发展的正确方法; 我在网上发现的很多讨论都表明他们试图在Python 3中废除args.

更新:两个答案建议覆盖__init__,和__str__/ __unicode__/ __repr__.这似乎很多打字,是否有必要?

gah*_*ooa 1208

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

class MyException(Exception):
    pass
Run Code Online (Sandbox Code Playgroud)

编辑:覆盖某些内容(或传递额外的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
Run Code Online (Sandbox Code Playgroud)

这样你可以将错误消息的dict传递给第二个参数,稍后再使用 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
Run Code Online (Sandbox Code Playgroud)

  • @jiakai的意思是"可选".:-) (62认同)
  • 但是,这样定义的异常是不可接受的; 请参阅此处的讨论/sf/ask/1137144641/ (28认同)
  • 这是一个误解,@ddleon。您所引用的文档中的示例适用于特定用例。子类的构造函数参数的名称(及其数量)没有任何意义。 (6认同)
  • 根据有关用户定义异常的 python 文档,\_\_init\_\_ 函数中提到的名称不正确。它不是(自我,消息,错误),而是(自我,表达式,消息)。属性表达式是发生错误的输入表达式,消息是对错误的解释。 (3认同)
  • 我错过了什么关于能够传递错误消息字典有多棒? (3认同)

frn*_*stn 458

随着现代Python的例外,你并不需要滥用.message,或覆盖.__str__().__repr__()或任何它.如果你想要的只是提出异常的信息,请执行以下操作:

class MyException(Exception):
    pass

raise MyException("My hovercraft is full of eels")
Run Code Online (Sandbox Code Playgroud)

这将以追溯结束MyException: My hovercraft is full of eels.

如果您希望从异常中获得更大的灵活性,可以将字典作为参数传递:

raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})
Run Code Online (Sandbox Code Playgroud)

但是,要在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"])
Run Code Online (Sandbox Code Playgroud)

仍然可以将多个项目传递给异常并通过元组索引访问它们,但这是非常不鼓励的(甚至打算暂时弃用).如果您确实需要多条信息并且上述方法对您来说还不够,那么您应该Exception按照教程中的描述进行子类化.

class MyError(Exception):
    def __init__(self, message, animal):
        self.message = message
        self.animal = animal
    def __str__(self):
        return self.message
Run Code Online (Sandbox Code Playgroud)

  • PEP352 的相关部分是 ["Retracted Ideas"](https://www.python.org/dev/peps/pep-0352/#retracted-ideas)。 (3认同)
  • “但这将在未来被弃用” - 这仍然是为了弃用吗?Python 3.7 似乎仍然很乐意接受`Exception(foo, bar, qux)`。 (2认同)
  • @neves一开始,使用元组存储异常信息与使用字典执行相同操作没有任何好处.如果您对异常更改背后的原因感兴趣,请查看[PEP352](https://www.python.org/dev/peps/pep-0352/) (2认同)

Aar*_*all 186

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

这很好,除非您的异常实际上是一种更具体的异常:

class MyException(Exception):
    pass
Run Code Online (Sandbox Code Playgroud)

或者更好(也许更完美),而不是pass给文档字符串:

class MyException(Exception):
    """Raise for my specific kind of exception"""
Run Code Online (Sandbox Code Playgroud)

子类化异常子类

来自文档

Exception

所有内置的,非系统退出的异常都派生自此类.所有用户定义的异常也应该从该类派生.

这意味着如果您的异常是一种更具体的异常,则将该异常子类化为通用Exception(并且结果将是您仍然从Exception文档推荐的那样派生的).此外,您至少可以提供docstring(而不是强制使用该pass关键字):

class MyAppValueError(ValueError):
    '''Raise when my specific value is wrong'''
Run Code Online (Sandbox Code Playgroud)

使用自定义设置您自己创建的属性__init__.避免传递dict作为位置参数,未来的代码用户会感谢你.如果您使用已弃用的邮件属性,则自行分配将避免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) 
Run Code Online (Sandbox Code Playgroud)

真的没有必要写自己的__str____repr__.内置的非常好,你的合作继承确保你使用它.

对最佳答案的批判

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

class MyException(Exception):
    pass
Run Code Online (Sandbox Code Playgroud)

同样,上面的问题是,为了捕获它,你要么必须专门命名(如果在其他地方创建就导入它)或捕获异常,(但你可能不准备处理所有类型的异常,你应该只捕捉你准备处理的异常).类似的批评下面,但另外这不是初始化通过的方式super,DeprecationWarning如果你访问消息属性,你会得到一个:

编辑:覆盖某些内容(或传递额外的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
Run Code Online (Sandbox Code Playgroud)

这样你就可以将错误消息的dict传递给第二个参数,并在以后通过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'
Run Code Online (Sandbox Code Playgroud)

相比:

>>> MyAppValueError('foo', 'FOO', 'bar').message
'foo'
Run Code Online (Sandbox Code Playgroud)

  • @Kos对Liskov Substitutability的批评仍然有效.第一个论点作为"信息"的语义也可以说是有问题的,但我认为我不会说这一点.当我有更多的空闲时间时,我会更多地看一看. (4认同)
  • 我认为遵循带有自定义例外的里氏替换原则没有多大意义。您提出特定的异常来指示特定的情况。为什么需要用派生异常类的实例替换基异常类的实例? (3认同)
  • 您好,2018年!Python 3中不再提供BaseException.message,因此批注仅适用于旧版本,对吗? (2认同)
  • @ostergaard 现在无法完整回答,但简而言之,用户可以获得捕获 `ValueError` 的附加选项。如果它属于值错误类别,这是有道理的。如果它不属于值错误的范畴,我会在语义上反对它。对于程序员来说,有一些细微差别和推理的空间,但我更喜欢适用时的特异性。我会尽快更新我的答案以更好地解决这个问题。 (2认同)
  • @AaronHall 我在实践中从来不需要这个。不管怎样,我猜你的例子也违反了LSP:如果你的代码使用了“raise ValueError(msg)”,你就不能用“raise ValidationError(msg)”来替换它,因为后者还需要一个参数。 (2认同)
  • 与@Eugene所说的相关,[里氏替换原则是否适用于构造函数](https://softwareengineering.stackexchange.com/q/302476/330471)?(另请参阅:[此](/sf/ask/384357711/),[此](https://softwareengineering.stackexchange.com /q/270734/330471).) 具体来说,在出现异常的情况下,我很可能决定用更具体的表达式替换通用表达式,但在这种情况下,我还将确保提供必要的论点——否则,这就是一个半生不熟的工作。 (2认同)

myk*_*hal 47

见异常缺省情况下是如何工作的,如果一个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')
Run Code Online (Sandbox Code Playgroud)

所以你可能希望有一种" 异常模板 ",以兼容的方式作为异常本身:

>>> 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')
Run Code Online (Sandbox Code Playgroud)

这可以通过这个子类轻松完成

class ExceptionTemplate(Exception):
    def __call__(self, *args):
        return self.__class__(*(self.args + args))
# ...
class NastyError(ExceptionTemplate): pass
Run Code Online (Sandbox Code Playgroud)

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

    # ...
    def __str__(self):
        return ': '.join(self.args)
Run Code Online (Sandbox Code Playgroud)

你会的

>>> raise nastyerr('code is broken')
NastyError: bad thing happened: code is broken
Run Code Online (Sandbox Code Playgroud)


Eug*_*ash 24

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

  • 定义基类继承Exception。这将允许轻松捕获与项目相关的任何异常:

    class MyProjectError(Exception):
        """A base class for MyProject exceptions."""
    
    Run Code Online (Sandbox Code Playgroud)

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

  • 要创建特定异常,请子类化基本异常类。

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

    class CustomError(MyProjectError):
        def __init__(self, *args, **kwargs):
            super().__init__(*args)
            self.foo = kwargs.get('foo')
    
    Run Code Online (Sandbox Code Playgroud)

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

     raise CustomError('Something bad happened', foo='foo')
    
    Run Code Online (Sandbox Code Playgroud)

此设计遵循Liskov 替换原则,因为您可以用派生异常类的实例替换基本异常类的实例。此外,它还允许您使用与父类相同的参数创建派生类的实例。

  • 真的很喜欢这个设计......我觉得它比其他答案中的干净很多。 (3认同)
  • LSP 粘附应该是强制性的,这就是为什么我更喜欢这个答案而不是其他答案。 (2认同)

fam*_*man 19

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

class CustomExceptionName(Exception):
    """Exception raised when very uncommon things happen"""
    pass
Run Code Online (Sandbox Code Playgroud)

请不要忘记记录,为什么需要自定义例外!

如果需要,这是获取更多数据的异常的方法:

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
Run Code Online (Sandbox Code Playgroud)

并像他们一样取出它们:

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
Run Code Online (Sandbox Code Playgroud)

payload=None使它成为可腌制的重要.在转储它之前,你必须打电话error.__reduce__().加载将按预期工作.

return如果需要将大量数据传输到某些外部结构,您可能应该使用pythons 语句调查寻找解决方案.对我来说,这似乎更清晰/更pythonic.Java中使用了高级异常,当使用框架并且必须捕获所有可能的错误时,这有时会令人讨厌.

  • 诚实的问题:为什么异常可腌制很重要?转储和加载异常的用例有哪些? (5认同)
  • 嗯,Python 3.8对此做了哪些更改?出于好奇,我浏览了您发布的链接,但没有提及与此主题相关的任何信息... (3认同)
  • 至少,[当前文档](https://docs.python.org/3.8/tutorial/errors.html#user-defined-exceptions) 表明这是这样做的方法(至少没有`__str__ `) 而不是使用 `super().__init__(...)` 的其他答案.. 只是为了更好的“默认”序列化可能需要覆盖 `__str__` 和 `__repr__` 的遗憾。 (3认同)
  • @RoelSchroeven:我必须并行化代码一次。单个进程运行良好,但其某些类的某些方面不可序列化(lambda 函数作为对象传递)。我花了一些时间弄清楚并修复它。这意味着后来有人可能最终需要序列化你的代码,但无法做到这一点,并且必须找出原因......我的问题不是不可修复的错误,但我可以看到它导致类似的问题。 (2认同)

M. *_*AYA 17

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


Yar*_*nko 11

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

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

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

  • `总是仅使用一个参数调用 BaseException.__init__。` 似乎是不需要的约束,因为它实际上接受_任意数量_ 的参数。 (4认同)

Gal*_*ses 10

为了最大程度地自定义,为了定义自定义错误,您可能需要定义一个继承自Exception类的中间类,如下所示:

class BaseCustomException(Exception):
    def __init__(self, msg):
        self.msg = msg

    def __repr__(self):
        return self.msg


class MyCustomError(BaseCustomException):
    """raise my custom error"""

Run Code Online (Sandbox Code Playgroud)


See*_*roy 8

从 Python 3.9.5 开始,我对上述方法遇到了问题。但是,我发现这对我有用:

class MyException(Exception):
    """Port Exception"""
Run Code Online (Sandbox Code Playgroud)

然后它可以在如下代码中使用:

try:
    raise MyException('Message')

except MyException as err:
    print (err)
Run Code Online (Sandbox Code Playgroud)


Len*_*bro 7

不,"消息"不被禁止.它刚刚被弃用了.您的应用程序将使用消息正常工作.但是,当然,您可能希望摆脱弃用错误.

为应用程序创建自定义异常类时,其中许多不仅仅是从Exception继承,而是从其他类(如ValueError或类似的)继承.然后你必须适应他们对变量的使用.

如果你的应用程序中有很多例外,那么为所有这些例子设置一个通用的自定义基类通常是一个好主意,这样你的模块用户就可以做了

try:
    ...
except NelsonsExceptions:
    ...
Run Code Online (Sandbox Code Playgroud)

在这种情况下,您可以在__init__ and __str__那里执行所需的操作,因此您不必为每个例外重复它.但是简单地调用消息变量而不是消息就可以了.

在任何情况下,只需要__init__ or __str__执行与Exception本身不同的操作.因为如果弃用,则需要两者,否则会出错.这不是每个课程所需的额外代码.;)