何时应该声明自定义异常?

12
我希望抛出异常可以传达一些与错误相关的消息和值。我想知道何时宣布自定义异常最合适,而不是使用内置异常。
我已经看到许多像这样这个和许多类似的例子被推荐在其他网站上。
class NameTooShortError(ValueError):
    pass

def validate(name):
    if len(name) < 10:
        raise NameTooShortError(name)

我更倾向于编写以下代码:

def validate(name):
    if len(name) < 10:
        raise ValueError(f"Name too short: {name}")

如果需要在异常实例中存储复杂或特定的信息,我的直觉是仅声明自定义异常。对我来说,声明空类似乎是错误的。


不要认为空类有什么问题。至少在这种情况下...我也使用空异常类,在你的例子中这似乎是一个很好的用法。 - Tomerikoo
还有一种可能是我想混淆原始名称,例如假设为类RSAKeyAuthenticationFailedError创建一个名为AuthError的子类,以便使开发人员在调试时更容易理解错误消息。 - StardustGogeta
3个回答

7
有两个问题合并成一个: 我应该多久使用自定义异常(以免过度使用)?我是否应该优先选择自定义异常(而不是内置异常)? 让我们回答这两个问题。

过度使用自定义异常

你链接的 Dan Bader 的博客文章是一个很好的例子,说明了如何不应该做。这是自定义异常的滥用示例。每个异常类应该涵盖 一组相关的用途 (例如 ConfigError、BrowserError、DateParserError)。绝对不应该为每个需要引发异常的特定情况创建新的自定义异常。这就是异常消息的作用。

自定义 vs. 内置异常

这是一个更基于观点的话题,它也高度取决于特定的代码情境。我将展示两个有趣的例子(可能还有许多其他例子),在这些例子中,我认为使用自定义异常会更有益处。

01: 内部公开

让我们创建一个简单的 Web 浏览器模块(一个 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更改为其他库,那么你就必须修改所有捕获异常的模块。另一种捕获一些通用异常的替代方法也被不建议使用
如果在你的网络浏览器模块中创建自定义异常:
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类,因此您无需在Web浏览器模块中导入底层的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,这种情况就不会发生。

2
我已经按照你的错误遮蔽示例进行了操作,得到了你描述的结果...但我不明白为什么?验证函数没有引发ValueError,那么为什么会被捕获?编辑:哦,我懂了,在你的示例中,10实际上是1O,因此这是int()函数引发的ValueError。好样的! - Tom Sitter

2
创建自定义异常类...
最初的回答:创建自定义异常类是IT技术中的一项基本任务。
  • gives you a declarative inventory of all the expected errors your program may produce; can make maintenance a lot easier

  • allows you to catch specific exceptions selectively, especially if you establish a useful hierarchy of them:

    class ValidationError(ValueError):
        pass
    
    class NameTooShortError(ValidationError):
        pass
    
    ...
    
    class DatabaseError(RuntimeError):
        pass
    
    class DatabaseWriteError(DatabaseError):
        pass
    
  • allows you to separate presentation from code better: The message you put into the exception is not necessarily the message the end user will see, especially if you localise your app into multiple languages. With custom classes, you can write your frontend something like this (using generic common HTML template syntax, _() is the gettext localisation function):

    {% if isinstance(e, NameTooShortError) %}
      <p>{{ _('Entered name is too short, enter at least %d characters') % e.min_length }}</p>
    {% elif isinstance(...) %}
      ...
    {% else %}
      {# fallback for unexpected exceptions #}
      <p>{{ _('An error occurred: %s') % e }}</p>
    {% endif %}
    

    Try that with just ValueError(f'Name too short: {name}')


1

Ben,决定何时声明自定义异常是个人的选择。就我个人而言,当我遇到错误并且我盯着屏幕挠头不知道它确切含义或者出现某些广泛情况时,我喜欢使用自定义异常。在你提供的例子中,我个人会将其设置为“名称太短:{name},请输入超过10个字符的名称”。只是为了让最终用户能够理解为什么会出现这样的错误,因为他们可能不知道所需长度。希望这有所帮助:)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接