在Python中使用tryexceptelse是一种好的实践吗?

2024-04-29 21:25:55 发布

您现在位置:Python中文网/ 问答频道 /正文

在Python中,我不时看到以下块:

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

除了其他原因,尝试的原因是什么?

我不喜欢这种编程,因为它使用异常来执行流控制。然而,如果它被包含在语言中,肯定有一个很好的理由,不是吗

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

通常情况下,我将异常处理为:

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

Tags: 语言returnas编程exception原因thiselse
3条回答

"I do not know if it is out of ignorance, but I do not like that kind of programming, as it is using exceptions to perform flow control."

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

即使是Python核心开发人员也使用异常进行流控制,这种风格在语言中得到了充分的体现(即迭代器协议使用StopIteration来表示循环终止)

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

"It my understanding that exceptions are not errors, they should only be used for exceptional conditions"

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

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

换句话说,您对“异常是针对异常的”的理解在其他一些语言中是有意义的,但在Python中是没有意义的

"However, if it is included in the language itself, there must be a good reason for it, isn't it?"

除了有助于避免竞争条件外,异常对于将错误处理拉到循环之外也非常有用。在解释语言中,这是一个必要的优化,因为解释语言往往没有自动loop invariant code motion

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

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

另外,请参见堆栈溢出回答:Are exceptions really for exceptional errors?

"What is the reason for the try-except-else to exist?"

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 Runner中:

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

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

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

这个例子自然有点做作,但它显示了这种结构的一些情况

What is the reason for the try-except-else to exist?

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

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

示例

例如:

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与备选方案(可能会产生bug)进行比较,我们会发现它减少了代码行,并且我们可以拥有一个更可读、可维护和更少bug的代码库

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块中会隐藏该bug

我们希望最小化try块中的代码行,以避免捕获我们没有预料到的异常,其原则是,如果我们的代码失败,我们希望它失败。这是一个best practice

It is my understanding that exceptions are not errors

在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

一位评论人士问道:

Say you have a method which pings an external API and you want to handle the exception at a class outside the API wrapper, do you simply return e from the method under the except clause where e is the exception object?

不,您不返回异常,只需使用裸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中详细阐述

相关问题 更多 >