ChatGPT解决这个技术问题 Extra ChatGPT

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

在 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

R
Raymond Hettinger

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

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

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

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-block 保护。”这是这里最重要的学习
In the Python world, using exceptions for flow control is common and normal.——我认为值得区分“Python 世界”和“CPython 核心开发者世界”。我研究过很多 Python 代码库,很少看到用于流控制的异常,并且看到许多 Python 开发人员不鼓励这种使用。
“else 子句的另一个用例是执行在未发生异常时必须发生的操作,而在处理异常时不会发生的操作。”这对我帮助很大。谢谢!
P
Peter Mortensen

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 无论如何都会执行,即使正在使用 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中详细说明。


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

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

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

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


A
AbstProcDo

请参阅以下示例,该示例说明了有关 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 子句,而无需某人(可能很着急)阅读冗长的抽象解释。 (当然,当他们不再着急时,他们应该回来阅读完整的摘要。)
R
Russia Must Remove Putin

在 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,了解其中存在紧张的原则,并警惕过于依赖其中任何陈述的教条。


d
dcrosta

你应该小心使用 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 总是被执行,这就是为什么它可以通过总是设置一个默认值来发挥我们的优势,所以在异常的情况下它被返回,如果我们不想在异常的情况下返回这样的值,它足以删除最后一个块。顺便说一句,在异常捕获上使用传递是我永远不会做的事情:)
@Juan Antonio Gomez Moriano,我的编码块仅用于示例目的。我可能也永远不会使用 pass
h
hwjp

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

避免在 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

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

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


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

每当你看到这个:

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 的回答:stackoverflow.com/a/16138864/1197429(Raymond 是 Python 的核心贡献者,并且是 Pythonic 所有事物的权威!)
S
Smart Manoj

这是我关于如何理解 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

我认为这不能说明为什么你不能把 else 代码放在 try
R
RayLuo

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

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

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

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

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

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

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

它在最终...最终确定之前运行那个额外的代码块。 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', # <-- 为了演示起见, # 这里有一个拼写错误 %d 会触发异常。 # 如果你把这部分移到 try... 块中, # 流程会不必要地进入回滚路径。某事) finally: db.close() 在上面的示例中,您不能将成功的日志行移到 finally... 块后面。由于 else... 块内的潜在异常,您也不能将其完全移入 try... 块内。

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

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

def make_some_noise(duck):
    duck.quack()

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

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

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

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

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

它们的“爆炸半径”不同。返回值只对直接调用者可用;异常可以无限地自动转发,直到被捕获。它们的分布模式不同。根据定义,返回值是一段数据(即使您可以返回复合数据类型,例如字典或容器对象,但从技术上讲,它仍然是一个值)。相反,异常机制允许通过它们各自的专用通道返回多个值(一次一个)。在这里,每个 except FooError: ... 和 except BarError: ... 块都被视为自己的专用通道。

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

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

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

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

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

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

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

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


K
Kevin J. Rice

哦,你是对的。 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 子句中的语句期间的异常except 捕获。
if...except...else 更具可读性,否则您必须阅读“哦,在 try 块之后,无一例外,转到 try 块之外的语句”,因此使用 else: 倾向于在语义上稍微连接语法更好的海事组织。此外,最好将未捕获的语句留在初始 try 块之外。
@DougR。 “您正在检查 x = blah() 以查找异常源”,拥有 traceback 为什么要从错误的位置检查异常源?