【问题标题】:Python Exceptions: EAFP and What is Really Exceptional?Python 异常:EAFP 和什么是真正的异常?
【发布时间】:2011-03-06 10:25:18
【问题描述】:

在几个地方(herehere)已经说过 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 是否为空,但(也许)它比使用显式条件更易读。

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

【问题讨论】:

  • 您的代码对我来说看起来很奇怪。 heapify 将列表就地转换为堆。由于pq = heapq.heapify(a_list)pq 将是None
  • 你是对的!当我试图将我的实际代码提炼成适合示例的东西时,我犯了一个愚蠢的错误。现在已经修好了。

标签: python exception


【解决方案1】:

只能调用异常 真正的例外情况

不是在 Python 中:例如,每个 for 循环(除非它过早地breaks 或returns)被抛出和捕获的异常(StopIteration)终止。因此,每个循环发生一次的异常对 Python 来说并不陌生——它经常出现!

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

在这种情况下,我喜欢 Jon 的重写(应该通过删除 else 分支来进一步简化),因为它使代码更紧凑——这是一个务实的原因,绝对不是 Python 风格与外来原则的“调和”。

【讨论】:

  • 使用 EAFP,您如何避免因捕获与预期不同的异常而导致的静默错误?例如:try: r = f(x.a) except AttributeError: r = 0。你以为你只会在x 没有属性a 时分配r = 0。但是碰巧f 中的内部条件引发了相同的异常,而您在不知不觉中抓住了它。在将这些片段之一提供给try / except 之前,我真的必须将每个表达式分成原子部分吗?
  • Max,我认为在这种情况下最好的方法是在函数f 中添加一个try / accept 块,以便在内部抛出的AttributeError 到达外部处理程序之前捕获它。
【解决方案2】:

在大多数低级语言(如 C++)中,抛出异常的成本很高。这影响了很多关于异常的“常识”,并且不适用于在 VM 中运行的语言,如 Python。在 Python 中使用异常而不是条件的成本并不高。

(在这种情况下,“常识”成为一种习惯问题。人们从一种环境(低级语言)的经验中得出它,然后将其应用于新领域而不评估它是否有道理。)

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

但是,这往往会定义何时抛出异常。您说的是什么时候应该捕获异常。很简单,不用担心:异常并不昂贵,所以不要竭尽全力阻止它们被抛出。很多 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")

当然,我们可以测试并避免失败——但我们把事情复杂化了。我们试图猜测文件访问可能失败的所有方式(这并没有捕捉到所有这些方式),我们可能已经引入了竞争条件,并且我们正在做更多的 I/O 工作。这一切都是为我们完成的——只需捕获异常。

【讨论】:

    【解决方案3】:

    看着the docs我想你可以放心地重写函数如下:

    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。

    到达列表的末尾,这是您知道会在这里发生的事情不是例外 - 这是 garaunteed!所以更好的做法是提前处理它。如果您在另一个线程中有其他东西从同一个堆中消耗,那么使用 try-except 会更有意义(即处理特殊/不可预测的情况)。

    更一般地说,我会在可以测试的地方避免尝试异常,并提前避免失败。这迫使你说“我知道这种糟糕的情况可能会发生,所以这就是我如何处理它”。在我看来,你会因此倾向于编写更具可读性的代码。

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

    【讨论】:

      【解决方案4】:

      为了记录,我写的是这样的:

      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 到处抛出它们,我认为这种模式非常有用(甚至 pythonic)。

      【讨论】:

        【解决方案5】:

        我发现将异常用作“正常”流控制工具的做法在 Python 中已被广泛接受。它最常用于您所描述的情况,即当您到达某种序列的末尾时。

        在我看来,这是对异常的完全有效的使用。不过,您确实要小心使用异常处理。引发异常是一项相当昂贵的操作,因此最好确保您仅在序列结束时依赖异常,而不是在每次迭代中。

        【讨论】:

          猜你喜欢
          • 2011-03-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多