在 Python 中使用 try-except-else 是一个好习惯吗?

Posted

技术标签:

【中文标题】在 Python 中使用 try-except-else 是一个好习惯吗?【英文标题】:Is it a good practice to use try-except-else in Python? 【发布时间】:2013-04-14 19:56:31 【问题描述】:

在 Python 中时不时会看到块:

try:
   try_this(whatever)
except SomeException as exception:
   #Handle exception
else:
   return something

try-except-else 存在的原因是什么?

我不喜欢那种编程,因为它使用异常来执行流控制。但是,如果它包含在语言中,那肯定是有充分理由的,不是吗?

我的理解是异常不是错误,它们应该只用于异常情况(例如,我尝试将文件写入磁盘但没有更多空间,或者我没有权限),而不是用于流量控制。

通常我将异常处理为:

something = some_default_value
try:
    something = try_this(whatever)
except SomeException as exception:
    #Handle exception
finally:
    return something

或者如果发生异常我真的不想返回任何东西,那么:

try:
    something = try_this(whatever)
    return something
except SomeException as exception:
    #Handle exception

【问题讨论】:

【参考方案1】:

“我不知道是不是因为无知,但我不喜欢那样 一种编程,因为它使用异常来执行流控制。”

在 Python 世界中,使用异常进行流控制是常见且正常的。

即使是 Python 核心开发人员也使用异常来进行流控制,并且这种风格在语言中被广泛采用(即迭代器协议使用 StopIteration 来表示循环终止)。

此外,try-except 样式用于防止某些"look-before-you-leap" 构造中固有的竞争条件。例如,测试os.path.exists 会导致信息在您使用时可能已经过时。同样,Queue.full 返回可能是陈旧的信息。在这些情况下,try-except-else style 将生成更可靠的代码。

“我的理解是异常不是错误,它们应该只 用于特殊情况”

在其他一些语言中,该规则反映了其图书馆所反映的文化规范。 “规则”也部分基于这些语言的性能考虑。

Python 文化规范有些不同。在许多情况下,您必须对控制流使用异常。此外,在 Python 中使用异常不会像在某些编译语言中那样减慢周围代码和调用代码的速度(即CPython 已经在每一步都实现了用于异常检查的代码,无论您是否实际使用异常)。

换句话说,您对“例外是为例外”的理解是一个在其他一些语言中有意义的规则,但对于 Python 则不然。

"但是,如果它包含在语言本身中,则必须有一个 很好的理由,不是吗?”

除了有助于避免竞争条件外,异常对于将错误处理拉到循环外也非常有用。这是解释型语言的必要优化,这些语言不倾向于自动loop invariant code motion。

此外,在处理问题的能力与问题出现的地方相去甚远的常见情况下,异常可以大大简化代码。例如,通常有***用户界面代码调用业务逻辑代码,而这些代码又调用低级例程。低级例程中出现的情况(例如数据库访问中唯一键的重复记录)只能在***代码中处理(例如要求用户提供与现有键不冲突的新键)。对这种控制流使用异常允许中级例程完全忽略该问题,并与流控制的这方面很好地分离。

有一个nice blog post on the indispensibility of exceptions here。

另外,请参阅 Stack Overflow 答案:Are exceptions really for exceptional errors?

“try-except-else 存在的原因是什么?”

else 子句本身很有趣。它在没有例外但在 finally 子句之前运行。这是它的主要目的。

如果没有 else 子句,在最终确定之前运行附加代码的唯一选择是将代码添加到 try 子句的笨拙做法。这很笨拙,因为它有风险 在不打算受 try 块保护的代码中引发异常。

在最终确定之前运行额外的未受保护代码的用例并不经常出现。因此,不要期望在已发布的代码中看到很多示例。比较少见。

else 子句的另一个用例是执行在未发生异常时必须发生的操作,而在处理异常时不会发生的操作。例如:

recip = float('Inf')
try:
    recip = 1 / f(x)
except ZeroDivisionError:
    logging.info('Infinite result')
else:
    logging.info('Finite result')

另一个例子发生在unittest runners中:

try:
    tests_run += 1
    run_testcase(case)
except Exception:
    tests_failed += 1
    logging.exception('Failing test case: %r', case)
    print('F', end='')
else:
    logging.info('Successful test case: %r', case)
    print('.', end='')

最后,在 try 块中 else 子句最常见的用法是为了美化一点(将异常结果和非异常结果对齐在同一缩进级别)。这种用法始终是可选的,并不是绝对必要的。

【讨论】:

“这很笨拙,因为它有可能在代码中引发不应该受 try 块保护的异常。”这是这里最重要的学习 In the Python world, using exceptions for flow control is common and normal. -- 我认为值得区分“Python 世界”和“CPython 核心开发者世界”。我研究过很多 Python 代码库,很少看到用于流控制的异常,并且看到许多 Python 开发人员不鼓励这种使用。【参考方案2】:

try-except-else 存在的原因是什么?

try 块允许您处理预期的错误。 except 块应该只捕获您准备处理的异常。如果您处理意外错误,您的代码可能会做错事并隐藏错误。

如果没有错误,将执行else 子句,并且通过不在try 块中执行该代码,您可以避免捕获意外错误。同样,捕获意外错误可以隐藏错误。

示例

例如:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
else:
    return something

“try, except”套件有两个可选子句,elsefinally。所以实际上是try-except-else-finally

else 将仅在 try 块没有异常时进行评估。它允许我们简化下面更复杂的代码:

no_error = None
try:
    try_this(whatever)
    no_error = True
except SomeException as the_exception:
    handle(the_exception)
if no_error:
    return something

因此,如果我们将 else 与替代方案(可能会产生错误)进行比较,我们会发现它减少了代码行数,并且我们可以拥有更易读、可维护且错误更少的代码库。

finally

finally 无论如何都会执行,即使正在使用 return 语句评估另一行。

用伪代码分解

使用 cmets 以演示所有功能的最小可能形式将其分解可能会有所帮助。假设这个语法正确(但除非定义了名称,否则不可运行)伪代码在函数中。

例如:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle_SomeException(the_exception)
    # Handle a instance of SomeException or a subclass of it.
except Exception as the_exception:
    generic_handle(the_exception)
    # Handle any other exception that inherits from Exception
    # - doesn't include GeneratorExit, KeyboardInterrupt, SystemExit
    # Avoid bare `except:`
else: # there was no exception whatsoever
    return something()
    # if no exception, the "something()" gets evaluated,
    # but the return will not be executed due to the return in the
    # finally block below.
finally:
    # this block will execute no matter what, even if no exception,
    # after "something" is eval'd but before that value is returned
    # but even if there is an exception.
    # a return here will hijack the return functionality. e.g.:
    return True # hijacks the return in the else clause above

确实,我们可以else 块中的代码包含在 try 块中,如果没有异常,它将在其中运行,但如果代码本身引发了怎么办我们正在捕捉的那种例外?将其留在try 块中会隐藏该错误。

我们希望尽量减少try 块中的代码行数以避免捕获我们没有预料到的异常,原则是如果我们的代码失败,我们希望它大声失败。这是best practice。

我的理解是异常不是错误

在 Python 中,大多数异常都是错误。

我们可以使用 pydoc 查看异常层次结构。例如,在 Python 2 中:

$ python -m pydoc exceptions

或 Python 3:

$ python -m pydoc builtins

会给我们层次结构。我们可以看到大多数类型的Exception 都是错误,尽管Python 使用其中一些来完成for 循环(StopIteration) 之类的事情。这是 Python 3 的层次结构:

BaseException
    Exception
        ArithmeticError
            FloatingPointError
            OverflowError
            ZeroDivisionError
        AssertionError
        AttributeError
        BufferError
        EOFError
        ImportError
            ModuleNotFoundError
        LookupError
            IndexError
            KeyError
        MemoryError
        NameError
            UnboundLocalError
        OSError
            BlockingIOError
            ChildProcessError
            ConnectionError
                BrokenPipeError
                ConnectionAbortedError
                ConnectionRefusedError
                ConnectionResetError
            FileExistsError
            FileNotFoundError
            InterruptedError
            IsADirectoryError
            NotADirectoryError
            PermissionError
            ProcessLookupError
            TimeoutError
        ReferenceError
        RuntimeError
            NotImplementedError
            RecursionError
        StopAsyncIteration
        StopIteration
        SyntaxError
            IndentationError
                TabError
        SystemError
        TypeError
        ValueError
            UnicodeError
                UnicodeDecodeError
                UnicodeEncodeError
                UnicodeTranslateError
        Warning
            BytesWarning
            DeprecationWarning
            FutureWarning
            ImportWarning
            PendingDeprecationWarning
            ResourceWarning
            RuntimeWarning
            SyntaxWarning
            UnicodeWarning
            UserWarning
    GeneratorExit
    KeyboardInterrupt
    SystemExit

一位评论者问:

假设您有一个 ping 外部 API 的方法,并且您想在 API 包装器之外的类中处理异常,您是否只需从 except 子句下的方法中返回 e,其中 e 是异常对象?

不,您不返回异常,只需使用 raise 重新引发它以保留堆栈跟踪。

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    raise

或者,在 Python 3 中,您可以引发新异常并使用异常链接保留回溯:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    raise DifferentException from the_exception

我在my answer here详细说明。

【讨论】:

点赞!你通常在手柄部分做什么?假设您有一个 ping 外部 API 的方法,并且您想在 API 包装器之外的类中处理异常,您是否只需从 except 子句下的方法中返回 e,其中 e 是异常对象? @PirateApp 谢谢!不,不要返回它,你可能应该用一个简单的raise 再加注或进行异常链接 - 但这是更多的主题并在此处介绍:***.com/q/2052390/541136 - 我可能会在看到你之后删除这些 cmets'见过他们。 非常感谢您提供的详细信息!现在浏览帖子【参考方案3】:

Python 不认同异常只能用于异常情况的想法,实际上这个成语是'ask for forgiveness, not permission'。这意味着将异常用作流程控制的常规部分是完全可以接受的,实际上是值得鼓励的。

这通常是一件好事,因为以这种方式工作有助于避免一些问题(作为一个明显的例子,经常避免竞争条件),并且它往往使代码更具可读性。

想象一下,您有一些需要处理的用户输入,但有一个已处理的默认值。 try: ... except: ... else: ... 结构使得代码非常易读:

try:
   raw_value = int(input())
except ValueError:
   value = some_processed_value
else: # no error occured
   value = process_value(raw_value)

比较它在其他语言中的工作方式:

raw_value = input()
if valid_number(raw_value):
    value = process_value(int(raw_value))
else:
    value = some_processed_value

注意优点。无需检查值是否有效并单独解析,它们只完成一次。代码也遵循更合乎逻辑的进展,主要的代码路径是第一个,然后是'如果它不起作用,就这样做'。

这个例子自然有点做作,但它表明这种结构是有案例的。

【讨论】:

【参考方案4】:

请参阅以下示例,该示例说明了有关 try-except-else-finally 的所有内容:

for i in range(3):
    try:
        y = 1 / i
    except ZeroDivisionError:
        print(f"\ti = i")
        print("\tError report: ZeroDivisionError")
    else:
        print(f"\ti = i")
        print(f"\tNo error report and y equals y")
    finally:
        print("Try block is run.")

实施它并通过:

    i = 0
    Error report: ZeroDivisionError
Try block is run.
    i = 1
    No error report and y equals 1.0
Try block is run.
    i = 2
    No error report and y equals 0.5
Try block is run.

【讨论】:

这是一个很好的简单示例,它快速 演示了完整的 try 子句,而无需某人(可能很着急)阅读冗长的抽象解释。 (当然,当他们不再着急时,他们应该回来阅读完整的摘要。)【参考方案5】:

在python中使用try-except-else是个好习惯吗?

答案是它依赖于上下文。如果你这样做:

d = dict()
try:
    item = d['item']
except KeyError:
    item = 'default'

这表明你对 Python 不是很了解。此功能封装在dict.get 方法中:

item = d.get('item', 'default')

try/except 块是一种视觉上更加混乱和冗长的编写方式,可以使用原子方法在一行中有效地执行。在其他情况下,这是正确的。

然而,这并不意味着我们应该避免所有的异常处理。在某些情况下,最好避免竞争条件。不要检查文件是否存在,只需尝试打开它,然后捕获相应的 IOError。为了简单和可读性,尝试将其封装或将其分解为适当的。

阅读Zen of Python,了解其中存在紧张的原则,并警惕过于依赖其中任何陈述的教条。

【讨论】:

【参考方案6】:

你应该小心使用 finally 块,因为它与在 try 中使用 else 块不同,除了。无论 try except 的结果如何,都会运行 finally 块。

In [10]: dict_ = "a": 1

In [11]: try:
   ....:     dict_["b"]
   ....: except KeyError:
   ....:     pass
   ....: finally:
   ....:     print "something"
   ....:     
something

正如每个人都注意到的那样,使用 else 块会使您的代码更具可读性,并且仅在未引发异常时运行

In [14]: try:
             dict_["b"]
         except KeyError:
             pass
         else:
             print "something"
   ....:

【讨论】:

我知道 finally 总是被执行的,这就是为什么它可以通过总是设置一个默认值来发挥我们的优势,所以如果出现异常,如果我们不想返回它就会返回这样的值在异常的情况下,删除最后一个块就足够了。顺便说一句,对异常捕获使用 pass 是我永远不会做的事情:) @Juan Antonio Gomez Moriano ,我的编码块仅用于示例目的。我可能永远也不会使用 pass 【参考方案7】:

每当你看到这个:

try:
    y = 1 / x
except ZeroDivisionError:
    pass
else:
    return y

甚至这个:

try:
    return 1 / x
except ZeroDivisionError:
    return None

请考虑这个:

import contextlib
with contextlib.suppress(ZeroDivisionError):
    return 1 / x

【讨论】:

它没有回答我的问题,因为那只是我朋友的一个例子。 在 Python 中,异常不是错误。他们甚至都不例外。在 Python 中,使用异常进行流控制是正常和自然的。标准库中包含 contextlib.suppress() 证明了这一点。请在此处查看 Raymond Hettinger 的答案:***.com/a/16138864/1197429(Raymond 是 Python 的核心贡献者,并且是 Pythonic 的所有事物的权威!)【参考方案8】:

只是因为没有其他人发表过这个观点,我会说

避免try/excepts 中的else 子句因为大多数人不熟悉它们

与关键字tryexceptfinally 不同,else 子句的含义不言自明;它的可读性较差。因为它不经常使用,它会导致阅读您的代码的人想要仔细检查文档以确保他们了解发生了什么。

(我写这个答案正是因为我在我的代码库中找到了一个try/except/else,它引起了一个 wtf 时刻并迫使我做一些谷歌搜索)。

所以,无论我在哪里看到像 OP 示例这样的代码:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
else:
    # do some more processing in non-exception case
    return something

我更愿意重构为

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    return  # <1>
# do some more processing in non-exception case  <2>
return something

显式返回,清楚地表明,在异常情况下,我们已经完成了工作

作为一个很好的次要副作用,以前在 else 块中的代码被缩进了一级。

【讨论】:

魔鬼的论据反驳:使用的人越多,它的采用率就会越高。只是深思熟虑,尽管我同意可读性很重要。也就是说,一旦有人理解了 try-else,我认为它在许多情况下比替代方案更具可读性。 我很少相信任何基于“大多数人”可能/可能不知道的事来猜测自己的论点。我也高度怀疑迎合无知的论点。 try-else 是在 C/C++/Java 中找不到的特定于 Python 的构造。对于“大多数人”的一种特殊结构,大多数人都将 Python 编程为 C/C++/Java,而不是利用许多使 Python 优雅和强大的独特功能。因此,我们应该使用 Python 的一个笨拙/低效的子集来迎合他们吗?列表理解似乎也吸引了这个论点。我不买。【参考方案9】:

这是我关于如何理解 Python 中的 try-except-else-finally 块的简单 sn-p:

def div(a, b):
    try:
        a/b
    except ZeroDivisionError:
        print("Zero Division Error detected")
    else:
        print("No Zero Division Error")
    finally:
        print("Finally the division of %d/%d is done" % (a, b))

让我们试试 div 1/1:

div(1, 1)
No Zero Division Error
Finally the division of 1/1 is done

让我们试试 div 1/0

div(1, 0)
Zero Division Error detected
Finally the division of 1/0 is done

【讨论】:

我认为这不能说明为什么你不能把 else 代码放在 try 中【参考方案10】:

我试图从稍微不同的角度回答这个问题。

OP 的问题有两部分,我也添加了第三部分。

    try-except-else 存在的原因是什么? try-except-else 模式或一般的 Python 是否鼓励使用异常进行流控制? 到底什么时候使用异常?

问题一:try-except-else存在的原因是什么?

可以从战术的角度来回答。 try...except... 的存在当然是有原因的。这里唯一新增的是else... 子句,它的用处归结为它的独特性:

仅当try... 块中没有发生异常时,它才会运行一个额外的代码块。

它在try... 块之外运行那个额外的代码块(这意味着在else... 块内发生的任何潜在异常都不会被捕获)。

它在final... 完成之前运行那个额外的代码块。

  db = open(...)
  try:
      db.insert(something)
  except Exception:
      db.rollback()
      logging.exception('Failing: %s, db is ROLLED BACK', something)
  else:
      db.commit()
      logging.info(
          'Successful: %d',  # <-- For the sake of demonstration,
                             # there is a typo %d here to trigger an exception.
                             # If you move this section into the try... block,
                             # the flow would unnecessarily go to the rollback path.
          something)
  finally:
      db.close()

在上面的示例中,您不能将成功的日志行移到 finally... 块后面。由于else... 块内的潜在异常,您也不能将其完全移动到try... 块内。

问题 2:Python 是否鼓励使用异常进行流控制?

我没有找到支持该说法的官方书面文件。 (对于不同意的读者:请给 cmets 留下您找到的证据的链接。)我发现的唯一模糊相关的段落是 EAFP term:

EAFP

请求宽恕比请求许可更容易。这种常见的 Python 编码风格假设存在有效的键或属性,如果假设被证明是错误的,则捕获异常。这种干净快速的风格的特点是存在许多 try 和 except 语句。该技术与许多其他语言(如 C)常见的 LBYL 风格形成鲜明对比。

该段仅描述了这一点,而不是这样做:

def make_some_noise(speaker):
    if hasattr(speaker, "quack"):
        speaker.quack()

我们更喜欢这个:

def make_some_noise(speaker):
    try:
        speaker.quack()
    except AttributeError:
        logger.warning("This speaker is not a duck")

make_some_noise(DonaldDuck())  # This would work
make_some_noise(DonaldTrump())  # This would trigger exception

或者甚至可能省略尝试...除了:

def make_some_noise(duck):
    duck.quack()

因此,EAFP 鼓励鸭式打字。但它不鼓励使用异常进行流控制

问题 3:在什么情况下你应该设计你的程序来发出异常?

关于使用异常作为控制流是否是反模式尚无定论。因为,一旦为给定函数做出设计决策,它的使用模式也将被确定,然后调用者别无选择,只能以这种方式使用它。

所以,让我们回到基础,看看函数何时通过返回值或通过发出异常来更好地产生结果。

返回值和异常有什么区别?

    它们的“爆炸半径”不同。返回值只对直接调用者可用;异常可以无限距离自动转发,直到被捕获。

    它们的分布模式不同。根据定义,返回值是一段数据(即使您可以返回复合数据类型,例如字典或容器对象,但从技术上讲,它仍然是一个值)。 相反,异常机制允许通过它们各自的专用通道返回多个值(一次一个)。在这里,每个except FooError: ...except BarError: ... 块都被视为自己的专用通道。

因此,每个不同的场景都可以使用一种适合的机制。

所有正常情况最好通过返回值返回,因为调用者很可能需要立即使用该返回值。返回值方法还允许以函数式编程风格嵌套调用者层。异常机制的长爆炸半径和多个通道在这里没有帮助。 例如,如果任何名为 get_something(...) 的函数将其快乐路径结果作为异常生成,那将是不直观的。 (这并不是一个人为的例子。有one practice 来实现BinaryTree.Search(value) 以使用异常将值返回到深度递归的中间。)

如果调用者可能忘记处理返回值中的错误标记,使用异常的特征#2 将调用者从隐藏的错误中拯救出来可能是个好主意。一个典型的非示例是position = find_string(haystack, needle),不幸的是它的返回值-1null 往往会导致调用者出现错误。

如果错误标记会与结果命名空间中的正常值发生冲突,则几乎可以肯定会使用异常,因为您必须使用不同的通道来传达该错误。

如果正常通道,即返回值已经在快乐路径中使用,并且快乐路径没有复杂的流控制,你别无选择,只能使用异常进行流控制。人们一直在谈论 Python 如何使用 StopIteration 异常来终止迭代,并用它来证明“使用异常进行流控制”的合理性。但是恕我直言,这只是在特定情况下的实际选择,它并没有概括和美化“使用异常进行流量控制”。

此时,如果您已经对您的函数 get_stock_price() 是否只产生返回值或还会引发异常做出正确的决定,或者该函数是否由现有库提供,因此其行为早已被决定,你没有太多的选择来写它的调用者calculate_market_trend()。是否使用get_stock_price() 的异常来控制calculate_market_trend() 中的流程只是您的业务逻辑是否需要您这样做的问题。如果是,就去做;否则,让异常冒泡到更高的级别(这利用了异常的#1“长爆炸半径”特征)。

特别是,如果您正在实现一个中间层库Foo,并且您恰好依赖于较低级别的库Bar,您可能希望通过捕获所有@987654350 来隐藏您的实现细节@, Bar.ThatError, ...,并将它们映射到 Foo.GenericError。在这种情况下,长爆炸半径实际上对我们不利,因此您可能希望“仅当库 Bar 通过返回值返回其错误时”。不过话说回来,这个决定早就在Bar 中做出了,所以你可以忍受它。

总而言之,我认为是否使用异常作为控制流是一个有争议的问题。

【讨论】:

【参考方案11】:

OP,你是对的。 Python 中 try/except 之后的 else 很丑。它导致另一个不需要的流控制对象:

try:
    x = blah()
except:
    print "failed at blah()"
else:
    print "just succeeded with blah"

一个完全清楚的等价物是:

try:
    x = blah()
    print "just succeeded with blah"
except:
    print "failed at blah()"

这比 else 子句要清楚得多。 try/except 之后的 else 不经常写,所以需要一点时间来弄清楚它的含义。

仅仅因为你可以做一件事,并不意味着你应该做一件事。

许多功能已添加到语言中,因为有人认为它可能会派上用场。麻烦的是,功能越多,事情就越不清晰和明显,因为人们通常不使用那些花里胡哨的东西。

这里只有我的 5 美分。我必须跟在后面,清理很多大学一年级开发人员编写的代码,他们认为自己很聪明,想以某种超级紧凑、超级高效的方式编写代码,而这只会让事情变得一团糟稍后尝试阅读/修改。我每天投票支持可读性,周日投票两次。

【讨论】:

你是对的。这是完全清楚和等效的......除非它是你的 print 声明失败。如果x = blah() 返回str,但您的打印语句是print 'just succeeded with blah. x == %d' % x,会发生什么?现在你已经生成了一个TypeError,而你还没有准备好处理它;您正在检查 x = blah() 以查找异常的来源,但它甚至不存在。我已经不止一次地这样做了(或类似的),else 会阻止我犯这个错误。现在我知道得更清楚了。 :-D ...是的,你是对的。 else 子句不是一个漂亮的语句,在你习惯它之前,它并不直观。但是,当我第一次开始使用它时,finally 也不是... 为了呼应 Doug R.,它等价,因为 else 子句中的语句期间的异常被 @ 捕获987654333@. if...except...else 更具可读性,否则你必须阅读“哦,在 try 块之后,没有例外,转到 try 块之外的语句”,所以使用 else:倾向于在语义上更好地连接语法。此外,最好将未捕获的语句留在初始 try 块之外。 @DougR。 “您正在检查 x = blah() 以查找异常源”,拥有 traceback 为什么要从错误的位置检查异常源?

以上是关于在 Python 中使用 try-except-else 是一个好习惯吗?的主要内容,如果未能解决你的问题,请参考以下文章

在 python 中使用 soffice,Command 在终端中有效,但在 Python 子进程中无效

python 使用pymongo在python中使用MongoDB的示例

在 python 中使用命令行时出现语法错误

python 在python中使用全局变量

如何在 Python 3.x 和 Python 2.x 中使用 pip

在Python中使用Redis