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

2024-12-12 08:41:00
admin
原创
177
摘要:问题描述:在现代 Python 中声明自定义异常类的正确方法是什么?我的主要目标是遵循其他异常类所具有的任何标准,以便(例如)我包含在异常中的任何额外字符串都可以通过捕获异常的任何工具打印出来。我所说的“现代 Python”是指可以在 Python 2.5 中运行但对于 Python 2.6 和 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__需要输入很多内容,有必要吗?


解决方案 1:

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

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)

解决方案 2:

有了现代 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

解决方案 3:

“在现代 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__。避免将字典作为位置参数传递,您的代码的未来用户会感谢您。如果您使用已弃用的 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

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

解决方案 4:

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

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

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

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

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

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

您也可以将自定义异常类分为子类,以创建层次结构。

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

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

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

解决方案 5:

从 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使其可 pickle 非常重要。在转储之前,您必须调用error.__reduce__()。加载将按预期进行。

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

解决方案 6:

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

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

解决方案 7:

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

解决方案 8:

看到一篇非常好的文章《Python异常权威指南》。基本原则是:

  • 总是从(至少)Exception继承。

  • 始终BaseException.__init__仅使用一个参数进行调用。

  • 当构建一个库时,定义一个从Exception继承的基类。

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

  • 当有意义时,从内置异常类型继承。

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

解决方案 9:

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

当您为应用程序创建自定义异常类时,许多异常类并非仅从 Exception 派生子类,而是从其他类似异常派生子类ValueError。然后您必须适应它们对变量的使用。

如果你的应用程序中有很多异常,通常最好为所有异常创建一个通用的自定义基类,以便模块的用户可以这样做

try:
    ...
except NelsonsExceptions:
    ...

在这种情况下,您可以在那里执行__init____str__需要,这样您就不必为每个异常重复此操作。但只需将消息变量称为消息以外的其他名称即可。

无论如何,如果您执行的操作与 Exception 本身的操作不同,则只需要__init____str__。并且如果弃用,则您需要两者,否则会出错。每个类不需要很多额外的代码。

解决方案 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"""

解决方案 11:

从 Python 3.9.5 开始,我对上述方法感到困惑。不过,我发现这个对我有用:

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

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

try:
    raise MyException('Message')

except MyException as err:
    print (err)

解决方案 12:

可以使用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

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

解决方案 13:

一个非常简单的方法:

class CustomError(Exception):
    pass

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

或者,让错误出现但不打印__main__(可能看起来更干净、更整洁):

class CustomError(Exception):
    __module__ = Exception.__module__

raise CustomError("Improved CustomError!")

解决方案 14:

尝试此示例

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

解决方案 15:

我偶然发现了这个帖子。这是我处理自定义异常的方法。虽然这个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: My hovercraft is full of eels.

解决方案 16:

同意@sultanorazbayev的回答。为了简单起见,应该使用数据类

import logging
from dataclasses import dataclass

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

def main():
    @dataclass
    class MyCustomException(Exception):
        code: int
        message: str
        description: str

    try:
        raise MyCustomException(
            404, 
            "Resource not found", 
            "This resource does not exist or unavailable"
        )
    except MyCustomException as e:
        logger.info(
            f"""
            MyCustomException caught: 
            {e.code} 
            {e.message} 
            {e.description}
        """
        )
    finally:
        logger.info("Cleaning up and exiting...")


if __name__ == "__main__":
    main()

解决方案 17:

有几个答案都有很好的观点,但我仍在努力清楚地了解 Python 在 v3.13 中推荐的自定义异常方法。我还觉得其他答案大多涉及方法的各个方面,而没有对实现和用法进行完整的描述。

冒着使这个问题变得拥挤的风险,我将我当前的方法(使用 Python v3.10)与使用场景结合起来,尝试整理最新的建议。

此示例具有适合 REST API 实现但可以重新利用的特性。

据我所知,建议的原则是:

  1. 子类内置Exception

  2. super().__init__使用一个str参数调用

  3. 实现__str__对异常显示的控制

  4. 针对特定用例对自定义异常进行子类化

此示例未处理子异常的附加参数。对此疏忽深表歉意。此示例确实支持附加类属性,这些属性无需传递参数即可提供详细信息。

在使用中,我有时会将内置异常转换为自定义异常,以实现处理的统一。

异常.py

"""Exceptions identify application status and primary error message.

Exceptions are used to abort processing
and return execution to the handler whenever processing is blocked.
An Exception is also returned when processing is completed
to indicate HTTP 200 status to caller with or without a raise.

- https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
- https://docs.python.org/3/library/http.html#http.HTTPStatus
"""

import http
import sys
import traceback


class APIException(Exception):
    """Response message and HTTP status. Always subclass, do not use in code."""

    message: str = http.HTTPStatus.IM_A_TEAPOT.phrase  # One standard Exception argument
    code: int = http.HTTPStatus.IM_A_TEAPOT.value  # code is not a parameter

    def __init__(self, message: str = "") -> None:
        """Provide reliably located customisable message."""
        self.message = message or self.message
        super().__init__(self.message)

    def __str__(self) -> str:
        return str(self.code) + ": " + self.message + ": " + self.location(self)

    @staticmethod  # Can pass Exception that are not APIException
    def location(e: Exception | None = None) -> str:
        """Return string with ``traceback`` module and line number e.g. ``"init#67"``."""
        tb = e and e.__traceback__ or sys.exc_info()[-1]  # Use provided or current Exception
        if not tb:  # Exception instances that are not raised do not have a traceback
            return ""
        frame = traceback.extract_tb(tb)[-1]
        return frame[2] + "#" + str(frame[1])


class ServerError(APIException):
    code = http.HTTPStatus.INTERNAL_SERVER_ERROR.value  # HTTP 500
    message = "Server error"


class NotImplemented(APIException):  # noqa: A001: not shadowing builtin
    code = http.HTTPStatus.NOT_IMPLEMENTED.value  # HTTP 501
    message = "Not Implemented"


class BadRequest(APIException):
    code = http.HTTPStatus.BAD_REQUEST.value  # HTTP 400
    message = "Invalid request or parameters"


class NotFound(BadRequest):
    code = http.HTTPStatus.NOT_FOUND.value  # HTTP 404
    message = "Requested calculation not found"


class OK(APIException):
    code = http.HTTPStatus.OK.value  # HTTP 200
    message = http.HTTPStatus.OK.phrase


# vim: set list colorcolumn=121:

为了说明一些常见用法,将应用程序代码包装在一个try块中并处理来自不同来源的各种错误,此示例具有单个捕获序列,并且所有路径始终以变量APIException引用的定义实例退出status。这也适用exceptions.OK于成功路径。status然后始终包含status.code状态int代码和status.message响应描述,这主要用于"OK"成功响应。

处理程序.py

try:

    ...

except json.JSONDecodeError as e:
    status = exceptions.BadRequest("JSONDecodeError: " + str(e))
except pydantic.ValidationError as e:
    errors = e.errors()[0]
    msg = ": ".join((str(errors["msg"]), str(errors["loc"]), str(errors["type"])))
    msg = "ValidationError: {}".format(msg)
    status = exceptions.BadRequest(msg)
except exceptions.APIException as e:
    status = e
    status.message = type(e).__name__ + ": " + e.message
except Exception as e:  # noqa: BLE001 do catch all Exception in this case
    traceback.print_exc()  # Allow tracebacks to console
    msg = "Unexpected {}: {}: {}"
    msg = msg.format(type(e).__name__, str(e), exceptions.APIException.location(e))
    status = exceptions.ServerError(msg)

...

请注意如何APIException location从类中使用静态方法从其他异常中返回统一的字符串:

exceptions.APIException.location(<any Exception>)

解决方案 18:

对我来说,它只是__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
相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1579  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1355  
  信创产品在政府采购中的占比分析随着信息技术的飞速发展以及国家对信息安全重视程度的不断提高,信创产业应运而生并迅速崛起。信创,即信息技术应用创新,旨在实现信息技术领域的自主可控,减少对国外技术的依赖,保障国家信息安全。政府采购作为推动信创产业发展的重要力量,其对信创产品的采购占比情况备受关注。这不仅关系到信创产业的发展前...
信创和国产化的区别   8  
  信创,即信息技术应用创新产业,旨在实现信息技术领域的自主可控,摆脱对国外技术的依赖。近年来,国货国用信创发展势头迅猛,在诸多领域取得了显著成果。这一发展趋势对科技创新产生了深远的推动作用,不仅提升了我国在信息技术领域的自主创新能力,还为经济社会的数字化转型提供了坚实支撑。信创推动核心技术突破信创产业的发展促使企业和科研...
信创工作   9  
  信创技术,即信息技术应用创新产业,旨在实现信息技术领域的自主可控与安全可靠。近年来,信创技术发展迅猛,对中小企业产生了深远的影响,带来了诸多不可忽视的价值。在数字化转型的浪潮中,中小企业面临着激烈的市场竞争和复杂多变的环境,信创技术的出现为它们提供了新的发展机遇和支撑。信创技术对中小企业的影响技术架构变革信创技术促使中...
信创国产化   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用