在现代 Python 中声明自定义异常的正确方法?
- 2024-12-12 08:41:00
- admin 原创
- 177
问题描述:
在现代 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
类有点复杂,但它使得使用可变参数声明自定义表达异常变得很简单。
FinalViolation
,SingletonViolation
都是的子类,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."
FinalViolation
,SingletonViolation
不幸的是只接受 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 实现但可以重新利用的特性。
据我所知,建议的原则是:
子类内置
Exception
super().__init__
使用一个str
参数调用实现
__str__
对异常显示的控制针对特定用例对自定义异常进行子类化
此示例未处理子异常的附加参数。对此疏忽深表歉意。此示例确实支持附加类属性,这些属性无需传递参数即可提供详细信息。
在使用中,我有时会将内置异常转换为自定义异常,以实现处理的统一。
异常.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