Python 异常:EAFP 和什么是真正例外?

17 投票
5 回答
3141 浏览
提问于 2025-04-16 00:15

在一些地方提到过(这里这里),Python提倡的“请求原谅总比请求许可更简单”(EAFP)这个理念,应该和“异常只在真正特殊的情况下使用”这个想法结合起来。想象一下,我们在一个优先队列上进行弹出和推入操作,直到只剩下一个元素:

import heapq
...
pq = a_list[:]
heapq.heapify(pq)
while True:
    min1 = heapq.heappop(pq)
    try:
        min2 = heapq.heappop(pq)
    except IndexError:
        break
    else
        heapq.heappush(pq, min1 + min2)
# do something with min1

在循环中,异常只在len(a_list)次迭代时被触发一次,但这并不算真正的特殊情况,因为我们知道这最终会发生。这种做法让我们避免了多次检查a_list是否为空,但(可能)它的可读性不如使用明确的条件来得好。

对于这种非特殊的程序逻辑,大家对使用异常的看法是什么呢?

5 个回答

7

根据文档,我觉得你可以安全地把这个函数改写成下面这样:

import heapq
...
pq = heapq.heapify(a_list)
while pq:
    min1 = heapq.heappop(pq)
    if pq:
        min2 = heapq.heappop(pq)
        heapq.heappush(pq, min1 + min2)
# do something with min1

这样就可以避免使用try-except了。

在一个列表的末尾结束,这种情况是你知道会发生的,这并不是例外情况——这是肯定会发生的!所以更好的做法是提前处理这种情况。如果你有其他线程在同一个堆中消费数据,那么在那里的使用try-except就更有意义了(也就是说,处理一些特殊或不可预测的情况)。

更一般来说,我会尽量避免使用try-except,除非我能提前测试并避免失败。这迫使你要说“我知道这种糟糕的情况可能会发生,所以我该如何处理”。在我看来,这样写出来的代码会更容易阅读。

[编辑] 根据Alex的建议更新了示例

9

在很多底层语言,比如C++,抛出异常是比较耗费资源的。这影响了人们对异常的“常识”,而在像Python这样的虚拟机语言中,这种影响就没那么大。在Python中,使用异常的成本并不高,和用条件语句相比,差别不大。

(这就是“常识”变成习惯的一个例子。人们从一种环境——底层语言中获得经验,然后把这些经验应用到新的领域,却没有考虑这样做是否合理。)

一般来说,异常还是比较特殊的。并不是说它们发生得不频繁,而是说它们是例外。异常通常会打断正常的代码流程,而大多数情况下,我们不想一个个处理这些异常,这就是异常处理器的作用。在这一点上,Python和C++以及其他有异常的语言是一样的。

不过,这也决定了什么时候会抛出异常。你在讨论的是什么时候应该捕获异常。简单来说,不用担心:在Python中,抛出异常并不昂贵,所以不需要费尽心思去避免它们的出现。很多Python代码都是围绕这个设计的。

我不同意Jon的建议,提前测试和避免异常。如果这样做能让代码更清晰,那当然可以。但在很多情况下,这只会让事情变得复杂——可能会导致重复检查和引入错误。例如,

import os, errno, stat

def read_file(fn):
    """
    Read a file and return its contents.  If the file doesn't exist or
    can't be read, return "".
    """
    try:
        return open(fn).read()
    except IOError, e:
        return ""

def read_file_2(fn):
    """
    Read a file and return its contents.  If the file doesn't exist or
    can't be read, return "".
    """
    if not os.access(fn, os.R_OK):
        return ""
    st = os.stat(fn)
    if stat.S_ISDIR(st.st_mode):
        return ""
    return open(fn).read()

print read_file("x")

当然,我们可以测试并避免失败,但这样做会让事情变得很麻烦。我们试图猜测文件访问可能失败的所有方式(而且这并不能捕捉到所有情况),可能还会引入竞争条件,并且我们做了更多的输入输出工作。这些都可以由系统自动处理——只需捕获异常就可以了。

34

异常情况应该只在真正特殊的情况下被调用。

但在Python中并不是这样。例如,每一个 for 循环(除非它提前 breakreturn)都是通过抛出并捕获一个异常(StopIteration)来结束的。所以,在每次循环中发生一次异常,对Python来说并不奇怪——这其实是很常见的!

这个原则在其他编程语言中可能很重要,但这并不是在Python中也要遵循这个原则的理由,因为这与Python的设计理念完全相悖。

在这种情况下,我喜欢Jon的改写(可以进一步简化,去掉else分支),因为这样让代码更简洁——这是一个实用的理由,绝对不是用一个外来的原则来影响Python的风格。

撰写回答