python异常:EAFP和真正的异常是什么?

Python Exceptions: EAFP and What is Really Exceptional?

有人在一些地方(这里和这里)说,python强调"请求宽恕比请求许可更容易"(eafp)的观点应该与这样的观点相调和,即只有在真正特殊的情况下才应该调用异常。考虑以下情况,我们将弹出并推送一个优先级队列,直到只剩下一个元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

例外只在EDOCX1的0次迭代循环中被提升一次,但它并不是真正的例外,因为我们知道它最终会发生。这个设置使我们不必检查EDCOX1 1是否是空的,但是(也许)它比使用显式条件更不可读。

对于这种非异常程序逻辑使用异常有什么共识?


exceptions should only be called in
truly exceptional cases

不在python中:例如,每个for循环(除非它过早地终止了breaks或returns),因为抛出和捕获了一个异常(StopIteration)。所以,每个循环发生一次的异常对于Python来说并不奇怪——它经常出现在那里!

所讨论的原则在其他语言中可能是至关重要的,但这绝对没有理由将该原则应用于Python,因为它与语言的精神非常相反。

在本例中,我喜欢jon的rewrite(应该通过删除else分支进一步简化),因为它使代码更加紧凑——这是一个实用的原因,而不是用一个异类原则来"回火"python风格。


在大多数低级语言(如C++)中,抛出异常是昂贵的。这影响了很多关于异常的"常识",并且对运行在VM中的语言(如Python)也没有太大影响。在Python中,使用异常而不是条件并没有那么大的开销。

(在这种情况下,"常识"变成了习惯。人们从一种类型的环境(低级语言)的经验中获得它,然后将它应用到新的领域,而不评估它是否有意义。)

一般来说,例外仍然是例外。这并不意味着它们不会经常发生;这意味着它们是例外。它们往往会从普通的代码流中分离出来,而且在大多数情况下,您不想一个接一个地处理它们——这就是异常处理程序的要点。在Python中,这一部分与C++和所有其他异常的语言都是相同的。

然而,这往往会定义何时抛出异常。你说的是什么时候应该抓住例外。很简单,不要担心:异常并不昂贵,所以不要想方设法阻止抛出异常。很多python代码都是围绕这个设计的。

我不同意乔恩的建议,即尝试提前测试并避免出现异常。如果它导致更清晰的代码,就像在他的例子中那样,那就好了。然而,在很多情况下,这只会使事情变得复杂——它可以有效地导致重复检查和引入错误。例如,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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")

当然,我们可以测试并避免失败,但是我们把事情搞得很复杂。我们试图猜测文件访问可能失败的所有方式(而这并不能全部捕获),我们可能引入了竞争条件,并且我们正在做更多的I/O工作。这都是为我们做的--抓住例外。


查看文档,我认为您可以安全地重新编写以下函数:

1
2
3
4
5
6
7
8
9
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,但这样做会更有意义(即处理特殊/不可预测的情况)。

更一般地说,我会避免在任何可以测试的地方尝试例外,并避免提前失败。这迫使你说,"我知道这种糟糕的情况可能会发生,所以下面是我如何处理它"。在我看来,这样做的结果是,您将倾向于编写更可读的代码。

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


就为了记录,我会这样写:

1
2
3
4
5
6
7
8
9
10
11
import heapq
a_list = range(20)
pq = a_list[:]
heapq.heapify(pq)
try:
    while True:
        min1 = heapq.heappop(pq)
        min2 = heapq.heappop(pq)
        heapq.heappush(pq, min1 + min2)
except IndexError:
    pass # we ran out of numbers in pq

异常可以留下一个循环(甚至是函数),您可以为此使用它们。因为python把它们扔到了任何地方,所以我认为这个模式非常有用(甚至是Python)。


我发现使用异常作为"普通"流控制工具的实践在Python中被广泛接受。当你到达某个序列的末尾时,它最常用于你描述的那种情况。

在我看来,这是一个完全正确的例外使用。不过,您确实希望谨慎地随意使用异常处理。引发异常是一个相当昂贵的操作,因此最好确保您只依赖于序列末尾的异常,而不是每次迭代中的异常。