519

在 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
4

11 回答 11

788

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

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

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

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

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

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

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

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

“但是,如果它包含在语言本身中,那肯定是有充分理由的,不是吗?”

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

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

这里有一篇很好的关于异常的必要性的博客文章

另外,请参阅此堆栈溢出答案:异常是否真的是异常错误?

“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')

另一个例子发生在单元测试运行器中:

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 子句用于美化(将异常结果和非异常结果对齐在同一缩进级别)。这种使用始终是可选的,并不是绝对必要的。

于 2013-04-22T03:13:38.423 回答
201

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

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

finally

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

用伪代码分解

它可能有助于将其分解,以尽可能最小的形式展示所有功能,并附上注释。假设这个语法正确(但除非名称已定义,否则不可运行)伪代码在函数中。

例如:

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块中的代码行数以避免捕获我们没有预料到的异常,原则是如果我们的代码失败,我们希望它大声失败。这是一个最佳实践

我的理解是异常不是错误

在 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

我在这里详细说明我的答案

于 2015-07-25T13:30:17.183 回答
48

Python 不赞成只在异常情况下使用异常的想法,实际上成语是“请求宽恕,而不是许可”。这意味着使用异常作为流程控制的常规部分是完全可以接受的,实际上是值得鼓励的。

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

想象一下,您有一个需要处理的用户输入,但有一个已经处理的默认值。该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

注意优点。无需检查值是否有效并单独解析,它们只完成一次。代码也遵循更合乎逻辑的进展,主要代码路径首先,然后是“如果它不起作用,请执行此操作”。

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

于 2013-04-22T01:47:55.260 回答
19

请参阅以下示例,该示例说明了有关 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.
于 2018-08-25T05:04:02.983 回答
16

在 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。为了简单和可读性,尝试将其封装或将其分解为适当的。

阅读《Python 之禅》,了解其中存在紧张的原则,并警惕过于依赖其中任何一种陈述的教条。

于 2015-02-03T16:54:25.177 回答
8

你应该小心使用 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"
   ....:
于 2013-04-22T02:31:17.210 回答
7

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

避免使用else条款,try/excepts 因为它们对大多数人来说都不熟悉

与关键字try, except, and不同finally,从句的含义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
  • <1> 显式返回,清楚地表明,在异常情况下,我们完成了工作

  • <2> 作为一个很好的次要副作用,曾经在else块中的代码被减少了一层。

于 2019-10-15T09:53:38.660 回答
6

每当你看到这个:

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
于 2017-01-13T23:40:49.893 回答
2

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

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
于 2017-11-24T03:22:27.423 回答
0

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

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

  1. try-except-else 存在的原因是什么?
  2. try-except-else 模式或一般的 Python 是否鼓励使用异常进行流控制?
  3. 无论如何,何时使用异常?

问题一: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...块后面。try...由于块内可能存在异常,您也不能将其完全移动到块内else...

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

我没有找到任何官方书面文件来支持这种说法。(对于不同意的读者:请留下您找到的证据链接的评论。)我发现的唯一模糊相关的段落是这个EAFP 术语

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:在什么情况下你应该设计你的程序来发出异常?

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

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

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

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

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

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

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

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

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

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

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

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

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

于 2021-03-25T02:25:57.473 回答
-4

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

于 2015-03-20T16:03:11.920 回答