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

Proper way to declare custom exceptions in modern Python?

提问人:Nelson 提问时间:8/24/2009 最后编辑:SultanOrazbayevNelson 更新时间:9/11/2023 访问量:1086488

问:

在现代 Python 中声明自定义异常类的正确方法是什么?我的主要目标是遵循其他异常类所具有的任何标准,以便(例如)我在异常中包含的任何额外字符串都会被捕获异常的任何工具打印出来。

我所说的“现代 Python”是指将在 Python 2.5 中运行的东西,但对于 Python 2.6 和 Python 3.* 的做事方式来说是“正确的”。我所说的“custom”是指一个对象,它可以包含有关错误原因的额外数据:一个字符串,也可能是与异常相关的其他一些任意对象。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

这似乎很疯狂,对名为 的属性具有特殊含义。我从 PEP-352 中收集到该属性在他们试图弃用的 2.5 中确实具有特殊含义,所以我想这个名字(而且只有那个)现在被禁止了?呸。BaseExceptionmessage

我也模糊地意识到它有一些神奇的参数,但我从来不知道如何使用它。我也不确定这是未来做事的正确方式;我在网上找到的很多讨论都表明他们正试图在 Python 3 中取消 args。Exceptionargs

更新:两个答案建议覆盖 ,和 //。这似乎是很多打字,有必要吗?__init____str____unicode____repr__

python-3.x 异常

评论

33赞 Anakhand 6/29/2020
我相信这是 Python 不遵循自己的格言之一的情况之一:There should be one-- and preferably only one --obvious way to do it.

答:

19赞 M. Utku ALTINKAYA 8/24/2009 #1

您应该重写 or 方法而不是使用 message,您在构造异常时提供的参数将位于异常对象的属性中。__repr____unicode__args

1899赞 gahooa 8/24/2009 #2

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

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)

评论

72赞 jiakai 8/1/2017
但是,像这样定义的例外是不可挑剔的;请参阅此处的讨论 stackoverflow.com/questions/16244923/...
3赞 ddleon 2/27/2020
按照 python 对用户定义的异常的文档,__init__ 函数中提到的名称不正确。它不是 (self,message,error),而是 (self,expression,message)。属性表达式是发生错误的输入表达式,消息是对错误的解释。
13赞 asthasr 4/23/2020
@ddleon,这是一种误解。您引用的文档中的示例适用于特定用例。子类的构造函数参数的名称(及其编号)没有意义。
3赞 martineau 12/17/2021
我错过了什么,能够传递错误消息的字典有多棒?
10赞 Lennart Regebro 8/24/2009 #3

不,“消息”不是被禁止的。它只是被弃用了。您的应用程序将很好地使用消息。但是,当然,您可能希望摆脱弃用错误。

当您为应用程序创建自定义 Exception 类时,其中许多类不仅从 Exception 中子类化,而且从其他类(如或类似)中子类化。然后你必须适应他们对变量的使用。ValueError

如果应用程序中有许多异常,通常最好为所有这些异常提供一个通用的自定义基类,以便模块的用户可以执行此操作

try:
    ...
except NelsonsExceptions:
    ...

在这种情况下,您可以在那里做并且需要,因此您不必对每个例外都重复它。但是,简单地将消息变量称为消息以外的其他变量就可以了。__init____str__

在任何情况下,您只需要或执行与 Exception 本身不同的操作。因为如果弃用,那么你需要两者,否则你会得到一个错误。这并不是每个类需要的大量额外代码。__init____str__

评论

1赞 Yaroslav Nikitenko 6/11/2019
有趣的是,Django 异常不会从公共基础继承。docs.djangoproject.com/en/2.2/_modules/django/core/exceptions当需要从特定应用程序捕获所有异常时,您有一个很好的例子吗?(也许它只对某些特定类型的应用程序有用)。
1赞 Yaroslav Nikitenko 6/11/2019
我找到了一篇关于这个主题的好文章,julien.danjou.info/python-exceptions-guide .我认为例外应该主要基于域,而不是基于应用程序。当应用与 HTTP 协议相关时,将从 HTTPError 派生。当应用的一部分是 TCP 时,可以从 TCPError 派生该部分的异常。但是,如果您的应用跨越多个域(文件、权限等),则使用 MyBaseException 的原因就会减少。还是为了防止“层违规”?
697赞 frnknstn 4/23/2012 #4

使用现代 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"})

但是,在一个块中获取这些细节有点复杂。详细信息存储在属性中,该属性是一个列表。您需要执行如下操作:exceptargs

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

评论

3赞 mtraceur 4/21/2018
“但这将来会被弃用”——这仍然打算弃用吗?Python 3.7 似乎仍然乐于接受。Exception(foo, bar, qux)
0赞 frnknstn 5/2/2018
自从上次尝试因过渡的痛苦而失败以来,它还没有看到任何最近的工作来剥夺它,但仍然不鼓励这种使用。我将更新我的答案以反映这一点。
0赞 neves 5/9/2018
@frnknstn,为什么不鼓励呢?对我来说,这似乎是一个不错的成语。
3赞 frnknstn 5/10/2018
@neves,首先,使用元组来存储异常信息与使用字典做同样的事情没有任何好处。如果您对异常更改背后的原因感兴趣,请查看 PEP352
4赞 liberforce 4/18/2019
PEP352 的相关部分是“撤回的想法”。
62赞 mykhal 8/8/2013 #5

查看默认情况下,如果使用一个或多个属性(省略回溯),异常是如何工作的:

>>> 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
308赞 Russia Must Remove Putin 11/15/2014 #6

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

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

class MyException(Exception):
    pass

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

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

子类化异常子类

文档

Exception

所有内置的、非系统退出的异常都派生自此类。 所有用户定义的异常也应由此派生 类。

这意味着,如果您的异常是更具体的异常类型,请对该异常进行子类化,而不是泛型异常(结果将是您仍然按照文档建议派生)。此外,您至少可以提供一个文档字符串(并且不会被迫使用关键字):ExceptionExceptionpass

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

设置您使用自定义 .避免将 dict 作为位置参数传递,您的代码的未来用户会感谢您。如果您使用已弃用的 message 属性,则自行分配该属性将避免:__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(但您可能没有准备好处理所有类型的异常,并且您应该只捕获您准备处理的异常)。与下面的批评类似,但除此之外,这不是通过 初始化的方法,如果您访问消息属性,您将得到一个:superDeprecationWarning

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

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'

评论

3赞 Kos 1/4/2018
2018年的你好! 在 Python 3 中消失了,所以批评只适用于旧版本,对吧?BaseException.message
11赞 Russia Must Remove Putin 1/4/2018
@Kos 关于李斯科夫可替代性的批评仍然有效。第一个论点作为“消息”的语义也可以说是值得怀疑的,但我认为我不会争论这一点。当我有更多的空闲时间时,我会多看看这个。
2赞 Russia Must Remove Putin 9/17/2018
@ostergaard 现在无法完整回答,但简而言之,用户获得了捕捉的额外选项。如果它属于“值错误”类别,则这是有道理的。如果它不属于价值错误的范畴,我会在语义上反对它。程序员有一些细微差别和推理的空间,但在适用的情况下,我更喜欢特异性。我会在不久的将来更新我的答案,以更好地解决这个问题。ValueError
4赞 Eugene Yarmash 2/28/2020
我认为遵循带有自定义例外的 Liskov 替换原则没有多大意义。引发特定异常以指示特定条件。为什么需要用派生异常类的实例替换基异常类的实例?
5赞 Anakhand 6/29/2020
与@Eugene所说的相关,里氏替代原则是否适用于构造函数?(参见:thisthis。具体来说,在异常的情况下,很可能我决定用更具体的表达式替换泛型表达式,但在这种情况下,我也会确保提供必要的参数——否则,这是一项半生不熟的工作。
4赞 omkaartg 7/22/2018 #7

试试这个例子

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")
64赞 fameman 11/26/2018 #8

从 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__()

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

评论

3赞 kevlarr 3/27/2019
至少,当前的文档表明这是这样做的方法(至少没有 ),而不是其他使用 .只是一个耻辱,它覆盖并且可能只是为了更好的“默认”序列化而必需的。__str__super().__init__(...)__str____repr__
9赞 Roel Schroeven 8/6/2019
诚实的问题:为什么例外情况是可腌制的?转储和加载异常有哪些用例?
3赞 logicOnAbstractions 10/6/2019
@RoelSchroeven:我必须并行化代码一次。运行良好的单个进程,但其某些类的某些方面不可序列化(lambda 函数作为对象传递)。我花了一些时间弄清楚并修复它。这意味着以后有人可能最终需要序列化您的代码,无法做到这一点,并且必须挖掘原因......我的问题不是无法选择的错误,但我可以看到它导致了类似的问题。
1赞 martineau 12/17/2021
(当前)链接的 3.8 文档中没有任何关于定义自定义异常的推荐方法的内容。
13赞 Yaroslav Nikitenko 6/11/2019 #9

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

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

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

评论

1赞 logicOnAbstractions 10/6/2019
这是一个很好的例子,说明为什么在 SO 上,我通常会检查投票最多的答案,但也检查最新的答案。有用的补充,谢谢。
5赞 Eugene Yarmash 2/29/2020
Always call BaseException.__init__ with only one argument.似乎是不需要的约束,因为它实际上接受任意数量的参数。
0赞 Yaroslav Nikitenko 2/29/2020
@EugeneYarmash我同意,现在我不明白。反正我不用它。也许我应该重读这篇文章并扩展我的答案。
0赞 Yaroslav Nikitenko 3/4/2020
@EugeneYarmash我又读了一遍这篇文章。据称,在多个参数的情况下,C 实现调用“return PyObject_Str(self->args);”这意味着一个字符串应该比多个字符串工作得更好。你检查了吗?
86赞 Eugene Yarmash 2/29/2020 #10

若要正确定义自己的例外,应遵循以下一些最佳做法:

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

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

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

  • 若要创建特定异常,请对基异常类进行子类化。

    class CustomError(MyProjectError):
       """A custom exception class for MyProject."""
    

    您也可以对自定义异常类进行子类化以创建层次结构。

  • 若要向自定义异常添加对额外参数的支持,请定义具有可变数量的参数的方法。调用基类的 ,将任何位置参数传递给它(请记住,BaseException/Exception 需要任意数量的位置参数)。将额外的关键字参数存储到实例中,例如:__init__()__init__()

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

    使用示例:

    try:
        raise CustomError('Something bad happened', custom_kwarg='value')
    except CustomError as exc:
        print(f'Сaught CustomError exception with custom_kwarg={exc.custom_kwarg}')
    

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

评论

9赞 LukeSavefrogs 7/14/2020
真的很喜欢这个设计...我觉得它比其他答案中的要干净得多。
2赞 loutre 7/18/2020
LSP 附着力应该是强制性的,这就是为什么我更喜欢这个答案而不是其他答案。
0赞 james 12/18/2020
我们如何测试这个异常是抛出还是不使用 unittests?
0赞 ingyhere 12/30/2020
这是可以腌制的吗?
1赞 Eugene Yarmash 12/17/2021
@ingyhere:是的,它应该是可挑剔的,没有问题(至少在 Python 3 中,这应该是现在的标准)。
4赞 Macintosh Fan 7/3/2020 #11

一个非常简单的方法:

class CustomError(Exception):
    pass

raise CustomError("Hmm, seems like this was custom coded...")

或者,在不打印的情况下引发错误(可能看起来更干净整洁):__main__

class CustomError(Exception):
    __module__ = Exception.__module__

raise CustomError("Improved CustomError!")
6赞 SeekNDstroy 10/13/2021 #12

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

class MyException(Exception):
    """Port Exception"""

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

try:
    raise MyException('Message')

except MyException as err:
    print (err)
1赞 Francis Cagney 12/14/2021 #13

我遇到了这个线程。这就是我做自定义异常的方式。虽然该类稍微复杂一些,但它使得使用变量参数声明自定义表达异常变得微不足道。Fault

FinalViolation,都是 so 的子类,因此将在下面捕获代码。SingletonViolationTypeError

try:
    <do something>
except TypeError as ex:
    <handler>

这就是为什么不继承自 .允许派生异常从其选择的异常继承。FaultException

class Fault:
    """Generic Exception base class. Note not descendant of Exception
Inheriting exceptions override formats"""
    formats = '' # to be overriden in descendant classes

    def __init__(self, *args):
        """Just save args for __str__"""
        self.args = args

    def __str__(self):
        """Use formats declared in descendant classes, and saved args to build exception text"""
        return self.formats.format(*self.args)

class TypeFault(Fault, TypeError):
    """Helper class mixing Fault and TypeError"""

class FinalViolation(TypeFault):
    """Custom exception raised if inheriting from 'final' class"""
    formats = "type {} is not an acceptable base type. It cannot be inherited from."

class SingletonViolation(TypeFault):     
    """Custom exception raised if instancing 'singleton' class a second time"""
    formats = "type {} is a singleton. It can only be instanced once."

FinalViolation,不幸的是只接受 1 个参数。SingletonViolation

但是可以很容易地创建一个多参数错误,例如

class VesselLoadingError(Fault, BufferError):
    formats = "My {} is full of {}."

raise VesselLoadingError('hovercraft', 'eels')

__main__.VesselLoadingError: My hovercraft is full of eels.

-2赞 Jirayu Kaewprateep 2/17/2022 #14

对我来说,这是公正和可变的,但有时会进行测试。__init__

我的样本:

Error_codes = { 100: "Not enough parameters", 101: "Number of special characters more than limits", 102: "At least 18 alphanumeric characters and list of special chars !@#$&*" }

class localbreak( Exception ) :
    Message = ""
    
    def __init__(self, Message):
        self.Message = Message
        return
    def __str__(self):
        print(self.Message)
        return "False"

### When calling ...
raise localbreak(Error_codes[102])

输出:

Traceback (most recent call last):   File "ASCII.py", line 150, in <module>
    main(OldPassword, Newpassword)   File "ASCII.py", line 39, in main
    result = read_input("1", "2", Newpassword, "4")                                     
    File "ASCII.py", line 69, in read_input
    raise localbreak(Error_codes[102]) At least 18 alphanumeric characters and list of special chars !@#$&*
__main__.localbreak: False
8赞 Galuoises 6/17/2022 #15

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

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

    def __repr__(self):
        return self.msg


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

1赞 SultanOrazbayev 7/16/2023 #16

可以使用它来简化自定义异常的定义:dataclass

from dataclasses import dataclass

@dataclass
class MyException(Exception):
    message: str = "This is a custom exception"

    def __str__(self):
        return f"Custom message: {self.message.upper()}"

raise MyException("abcdef")
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# MyException: Custom message: ABCDEF

raise MyException()
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# MyException: Custom message: THIS IS A CUSTOM EXCEPTION

这减少了一些样板,同时保持了进一步定制的灵活性。