ChatGPT解决这个技术问题 Extra ChatGPT

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

在现代 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__。这似乎是很多打字,有必要吗?

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

N
Nico Schlömer

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

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

这样,您可以将错误消息的 dict 传递给第二个参数,稍后使用 e.errors 获取它。

在 Python 2 中,您必须使用这种稍微复杂的 super() 形式:

super(ValidationError, self).__init__(message)

然而,像这样定义的异常是不可挑选的;请参阅此处的讨论stackoverflow.com/questions/16244923/…
@jiakai 的意思是“可腌制的”。 :-)
按照用户定义异常的python文档,__init__函数中提到的名称是不正确的。而不是 (self,message,error) 它是 (self,expression,message)。属性表达式是发生错误的输入表达式,消息是对错误的解释。
这是一个误解,@ddleon。您所指的文档中的示例适用于特定用例。子类的构造函数参数的名称(或其编号)没有意义。
关于能够传递错误消息的字典有多棒,我错过了什么?
f
frnknstn

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

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

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

“但这将在未来被弃用” - 这仍然是为了弃用吗? Python 3.7 似乎仍然乐于接受 Exception(foo, bar, qux)
自从上次尝试由于过渡的痛苦而失败以来,它还没有看到任何对其进行贬低的工作,但仍然不鼓励使用这种用法。我将更新我的答案以反映这一点。
@frnknstn,为什么不鼓励?对我来说似乎是一个不错的成语。
@neves 首先,使用元组来存储异常信息与使用字典来做同样的事情相比没有任何好处。如果您对异常更改背后的原因感兴趣,请查看 PEP352
PEP352 的相关部分是 "Retracted Ideas"
B
Bgil Midol

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

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

class MyException(Exception):
    pass

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

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

子类化异常子类

docs

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

这意味着如果您的异常是一种更具体的异常,则将该异常子类化而不是通用 Exception(结果将是您仍然按照文档建议从 Exception 派生)。此外,您至少可以提供一个文档字符串(而不是被迫使用 pass 关键字):

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

使用自定义 __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) 

确实没有必要编写自己的 __str____repr__。内置的非常好,您的合作继承确保您使用它们。

对最佳答案的批评

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

class MyException(Exception):
    pass

同样,上面的问题是,为了捕捉它,你要么必须专门命名它(如果在别处创建,则导入它)或捕捉异常,(但你可能不准备处理所有类型的异常,并且您应该只捕获您准备处理的异常)。与以下类似的批评,但另外这不是通过 super 初始化的方式,如果您访问 message 属性,您将获得 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

这样,您可以将错误消息的 dict 传递给第二个参数,然后使用 e.errors 获取它

它还需要传入两个参数(除了 self)。不多也不少。这是一个有趣的约束,未来的用户可能不会喜欢。

直截了当 - 它违反了 Liskov substitutability

我将演示这两个错误:

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

你好,从 2018 年开始! BaseException.message 在 Python 3 中消失了,所以批评只适用于旧版本,对吧?
@Kos 关于 Liskov 可替代性的批评仍然有效。作为“消息”的第一个参数的语义也可以说是有问题的,但我认为我不会争论这一点。当我有更多的空闲时间时,我会多看看这个。
@ostergaard 现在无法完整回答,但简而言之,用户可以获得捕获 ValueError 的附加选项。如果它属于值错误类别,这是有道理的。如果它不在值错误的类别中,我会在语义上反对它。程序员有一些细微差别和推理的空间,但我更喜欢适用时的特异性。我会更新我的答案,以便在不久的将来更好地解决这个问题。
我认为遵循带有自定义例外的 Liskov 替换原则没有多大意义。您引发特定异常以指示特定条件。为什么您需要用派生异常类的实例替换基异常类的实例?
与@Eugene 所说的does the Liskov substitution principle apply to constructors 相关吗? (另请参阅:thisthis。)具体而言,在例外情况下,我很可能决定用更具体的表达式替换泛型表达式,但在这种情况下,我也会一定要提供必要的论据——否则,这是一个半生不熟的工作。
m
mykhal

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

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

f
fameman

截至 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 return 语句来寻找解决方案。这对我来说似乎更清晰/更蟒蛇。 Java 中大量使用高级异常,当使用框架并且必须捕获所有可能的错误时,这有时会很烦人。


至少,current docs 表明这是这样做的方式(至少没有 __str__)而不是使用 super().__init__(...) 的其他答案。只是覆盖 __str____repr__ 的耻辱可能只是为了更好的“默认”序列化。
诚实的问题:为什么异常可以腌制很重要?转储和加载异常的用例是什么?
@RoelSchroeven:我不得不并行化一次代码。运行良好的单个进程,但其某些类的某些方面不可序列化(lambda 函数作为对象传递)。我花了一些时间弄清楚并修复它。这意味着以后有人可能最终需要对您的代码进行序列化,无法做到这一点,并且必须找出原因......我的问题不是不可挑剔的错误,但我可以看到它导致了类似的问题。
(当前)链接的 3.8 文档中没有关于定义自定义异常的推荐方法的任何内容。
E
Eugene Yarmash

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

定义一个继承自 Exception 的基类。这将允许轻松捕获与项目相关的任何异常: class MyProjectError(Exception): """A base class for MyProject exceptions.""" 在单独的模块中组织异常类(例如 exceptions.py)通常是一个好方法主意。

要创建特定异常,请对基异常类进行子类化。 class CustomError(MyProjectError): """MyProject 的自定义异常类。""" 您也可以继承自定义异常类以创建层次结构。

要为自定义异常添加对额外参数的支持,请定义具有可变数量参数的 __init__() 方法。调用基类的 __init__(),将任何位置参数传递给它(记住 BaseException/Exception 需要任意数量的位置参数)。为实例存储额外的参数,例如: class CustomError(MyProjectError): def __init__(self, *args, **kwargs): super().__init__(*args) self.foo = kwargs.get('foo ') 要使用额外的参数引发此类异常,您可以使用: raise CustomError('Something bad occurred', foo='foo')

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


真的很喜欢这个设计......我觉得它比其他答案中的更干净。
LSP 粘附应该是强制性的,这就是为什么我更喜欢这个答案而不是其他答案。
我们如何使用单元测试来测试这个异常是否被抛出?
这可以腌制吗?
@ingyhere:是的,它应该是无问题的(至少在 Python 3 中,这应该是现在的标准)。
b
bignose

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


Y
Yaroslav Nikitenko

请参阅一篇非常好的文章“The definitive guide to Python exceptions”。基本原则是:

总是从(至少)异常继承。

始终只使用一个参数调用 BaseException.__init__。

在构建库时,定义一个继承自 Exception 的基类。

提供有关错误的详细信息。

在有意义时从内置异常类型继承。

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


这是一个很好的例子,说明为什么我通常会检查最受好评的答案,但也会检查最新的答案。有用的补充,谢谢。
Always call BaseException.__init__ with only one argument. 似乎不需要约束,因为它实际上接受任意数量 个参数。
@EugeneYarmash 我同意,现在我不明白。反正我也不用。也许我应该重读这篇文章并扩展我的答案。
@EugeneYarmash 我再次阅读了这篇文章。据称,在有多个参数的情况下,C 实现调用“return PyObject_Str(self->args);”这意味着一个字符串应该比几个字符串更好。你检查了吗?
N
Neuron

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

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

而且,如果您的应用程序中有许多例外情况,通常最好为所有例外情况提供一个通用的自定义基类,以便您的模块的用户可以这样做

try:
    ...
except NelsonsExceptions:
    ...

在这种情况下,您可以在此处执行所需的 __init____str__,因此您不必为每个异常都重复它。但是简单地调用消息变量而不是消息就可以了。

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


有趣的是,Django 异常不是从一个共同的基础继承而来的。 docs.djangoproject.com/en/2.2/_modules/django/core/exceptions 在需要捕获来自特定应用程序的所有异常时,您有一个很好的例子吗? (也许它仅对某些特定类型的应用程序有用)。
我找到了一篇关于这个主题的好文章,julien.danjou.info/python-exceptions-guide。我认为异常应该主要基于域而不是基于应用程序进行子类化。当您的应用程序是关于 HTTP 协议时,您从 HTTPError 派生。当您的应用程序的一部分是 TCP 时,您从 TCPError 派生该部分的异常。但是,如果您的应用程序跨越很多域(文件、权限等),那么出现 MyBaseException 的原因就会减少。还是为了防止“违反层”?
o
omkaartg

试试这个例子

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

M
Macintosh Fan

一个非常简单的方法:

class CustomError(Exception):
    pass

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

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

class CustomError(Exception):
    __module__ = Exception.__module__

raise CustomError("Improved CustomError!")

G
Guido

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

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

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

try:
    raise MyException('Message')

except MyException as err:
    print (err)

G
Galuoises

为了最大限度地进行自定义,要定义自定义错误,您可能需要定义一个继承自 Exception 类的中间类:

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

    def __repr__(self):
        return self.msg


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


N
Neuron

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

FinalViolationSingletonViolation 都是 TypeError 的子类,因此将在下面捕获代码。

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

这就是 Fault 不继承自 Exception 的原因。允许派生异常从他们选择的异常中继承。

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."

FinalViolationSingletonViolation 很遗憾只接受 1 个参数。

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

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

raise VesselLoadingError('hovercraft', 'eels')

__main__.VesselLoadingError:我的气垫船装满了鳗鱼。


T
Tomerikoo

对我来说,它只是 __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