有两个问题合并为一个:我应该多久使用一次自定义异常(以免过度使用它们)? and 我真的应该更喜欢自定义异常(而不是内置异常)吗?我们两个都回答一下吧。
自定义异常过度使用
您链接的 Dan Bader 的博客文章是一个很好的例子,说明了不应这样做。过度使用自定义异常的示例。每个异常类应该涵盖一组相关的用途(配置错误、浏览器错误、日期解析器错误)。您绝对不应该为需要引发某些情况的每种特定情况创建新的自定义异常。这就是异常消息的用途。
自定义异常与内置异常
这是一个更加基于意见的主题,它也很大程度上取决于特定的代码场景。我将展示两个有趣的示例(可能有很多示例),我认为在这些示例中使用自定义异常可能是有益的。
01:内部暴露
让我们创建一个简单的 Web 浏览器模块(围绕Requests https://pypi.org/project/requests/包裹):
import requests
def get(url):
return requests.get(url)
现在假设您想要在包中的多个模块中使用新的 Web 浏览器模块。在其中一些中,您希望捕获一些可能与网络相关的异常:
import browser
import requests
try:
browser.get(url)
except requests.RequestException:
pass
该解决方案的缺点是您必须导入requests
封装在每个模块中只是为了捕获异常。您还暴露了浏览器模块的内部结构。如果您决定将底层 HTTP 库从 Requests 更改为其他库,则必须修改捕获异常的所有模块。捕获一些一般异常的替代方法也是灰心 https://stackoverflow.com/questions/4990718/about-catching-any-exception.
如果您在 Web 浏览器模块中创建自定义异常:
import requests
class RequestException(requests.RequestException):
pass
def get(url):
try:
return requests.get(url)
except requests.RequestException:
raise RequestException
那么您的所有模块现在都将避免上述缺点:
import browser
try:
browser.get(url)
except browser.RequestException:
pass
请注意,这也正是 Requests 包本身使用的方法 - 它定义了自己的方法RequestException
类,因此您不必导入底层urllib
打包到您的网络浏览器模块中只是为了捕获它引发的异常。
02:阴影错误
自定义异常不仅仅是为了让代码变得更漂亮。看看你的代码(稍作修改的版本),你会发现一些非常邪恶的东西:
def validate(name, value):
if len(name) < int(value):
raise ValueError(f"Name too short: {name}")
return name
现在有人会使用您的代码,但他宁愿捕获它并提供默认名称,而不是在短名称的情况下传播您的异常:
name = 'Thomas Jefferson'
try:
username = validate(name, '1O')
except ValueError:
username = 'default user'
代码看起来不错,不是吗?现在看这个:如果你改变name
变量实际上是任何字符串,username
变量将始终被设置为'default user'
。如果您定义并引发了自定义异常ValidationError
,这不会发生。