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

时间:2013-04-22 01:44:52

标签: python exception exception-handling try-catch

在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

11 个答案:

答案 0 :(得分:542)

  

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

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

即使Python核心开发人员也使用流控制的异常,并且该样式在语言中被大量使用(即迭代器协议使用StopIteration来表示循环终止)。

此外,try-except-style用于防止某些"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-clause中的笨拙做法。这很笨拙,因为它有风险 在代码中引发异常,而这些异常并不是由try-block保护的。

在最终确定之前运行其他未受保护的代码的用例不会经常出现。因此,不要期望在已发布的代码中看到许多示例。这有点罕见。

else子句的另一个用例是执行在没有异常发生时必须发生的操作,并且在处理异常时不会发生这些操作。例如:

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

最后,try-block中最常用的else子句是为了进行一些美化(在同一级别的缩进中调整异常结果和非异常结果)。此用法始终是可选的,并非绝对必要。

答案 1 :(得分:137)

  

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语句评估另一行。

使用伪代码

进行细分

通过评论,以尽可能最小的形式展示所有功能,可能有助于打破这种局面。假设这在语法上是正确的(但除非定义了名称,否则不可运行)伪代码在函数中。

例如:

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详细说明。

答案 2 :(得分:33)

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

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

这个例子自然有点人为,但它表明有这种结构的情况。

答案 3 :(得分:15)

  

在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,了解有些原则处于紧张状态,并且对过于依赖任何一种陈述的教条保持警惕。

答案 4 :(得分:7)

请参见以下示例,该示例说明了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.

答案 5 :(得分:5)

你应该小心使用finally块,因为它与在try中使用else块不同,除了。无论尝试的结果如何,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"
   ....:

答案 6 :(得分:4)

每当你看到这个:

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

答案 7 :(得分: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

答案 8 :(得分:2)

仅因为没有人发表过此意见,我会说

  

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

与关键字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
  • <1>显式返回,清楚地表明,在例外情况下,我们已经完成工作

  • <2>作为一个很好的次要副作用,以前在else块中的代码只分一级使用。

答案 9 :(得分:0)

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

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

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

问题 1: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 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

或者甚至可能省略 try...except:

def make_some_noise(duck):
    duck.quack()

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

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

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

那么,让我们回到基本原理,看看什么时候函数最好通过返回值或通过发出异常来产生其结果。

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

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

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

因此,使用一种适合的机制取决于每个不同的场景。

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

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

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

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

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

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

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

答案 10 :(得分:-3)

OP,你是正确的。 在Python之后的其他内容/在Python中除外是丑陋的。它导致了另一个不需要的流控制对象:

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美分。我必须坚持到底,并清理大学开发人员一年级编写的大量代码,他们认为他们很聪明,并希望以一种超级紧密,超级高效的方式编写代码。一团糟,以后尝试阅读/修改。我每天投票给可读性,周日投票两次。