在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
316957 次浏览

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

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

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

你应该小心使用finally块,因为它与在try中使用else块不是一回事,除了。finally块将运行,而不管try的结果是什么。

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"
....:
“我不知道这是否是出于无知,但我不喜欢这样 这是一种编程,因为它使用异常来执行流控制。" < / p >

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

即使是Python的核心开发人员也会使用异常来进行流控制,并且这种风格已经深深地嵌入到语言中(即迭代器协议使用< em >抛出StopIteration < / em >来表示循环终止)。

此外,try-except-样式用于防止某些“三思而后行”构造中固有的竞态条件。例如,测试< em > os.path.exists < / em >所得到的信息在您使用它时可能已经过时了。同样地,< em > Queue.full < / em >返回可能过时的信息。在这些情况下,try-except-else风格将生成更可靠的代码。

我的理解是异常不是错误,它们应该只是错误

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

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

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

"然而,如果它包含在语言本身中,则必须有一个 很好的理由,不是吗?" < / p >

除了有助于避免竞态条件,异常对于在外部循环中进行错误处理也非常有用。在解释性语言中,这是一个必要的优化,因为这些语言往往没有自动的循环不变码运动

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

有一个关于例外的必要性的博客文章不错

另外,请参阅Stack Overflow回答:异常真的是针对异常错误的吗?

“除了别的尝试存在的原因是什么?”

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

如果没有else-子句,在结束之前运行额外代码的唯一选项将是将代码添加到try-子句的笨拙实践。这是笨拙的,因为它有风险

.在代码中引发不受try-block保护的异常

在终结之前运行额外的不受保护代码的用例并不经常出现。因此,不要期望在已发布的代码中看到很多示例。这有点罕见。

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-block中使用else子句最常见的用途是进行一些美化(将异常结果和非异常结果对齐在同一缩进级别)。这种用法总是可选的,并不是严格必要的。

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

哦,你是对的。# EYZ0。它指向另一个不需要的流控制对象:

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分钱。我必须跟在后面,清理许多由大学一年级的开发人员编写的代码,这些开发人员自认为很聪明,想以一种超级紧凑、超级高效的方式编写代码,但这样做只会让以后尝试和阅读/修改时变得一团糟。我每天都为可读性投票,周日两次。

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

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

如果没有错误,则执行else子句,通过不执行try块中的代码,可以避免捕获意外错误。同样,捕捉意外错误可以隐藏错误。

例子

例如:

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

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

只有当try块没有异常时,else才会计算。它允许我们简化下面更复杂的代码:

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

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

# EYZ0

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块中会隐藏该错误。

我们希望减少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

我在我的答案是中详细说明。

当你看到这个:

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中的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

请看下面的例子,它说明了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/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块中的代码被降低了一级。

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

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术语:

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…除了:

def make_some_noise(duck):
duck.quack()

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

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

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

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

返回值和异常之间的区别是什么?

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

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

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

    所有正常情况下最好通过返回值返回,因为调用者很可能需要立即使用该返回值。返回值方法还允许以函数式编程风格嵌套调用方层。异常机制的长爆炸半径和多通道在这里没有帮助。 例如,如果任何名为get_something(...)的函数将其快乐路径结果作为异常产生,这将是不直观的。(这并不是一个虚构的例子。有一个练习来实现BinaryTree.Search(value)来使用异常在深度递归中间将值传送回来。
  • 如果调用者可能会忘记从返回值中处理错误哨兵,那么使用异常的characterist #2来避免调用者发现隐藏的错误可能是一个好主意。一个典型的非示例是position = find_string(haystack, needle),不幸的是,它的返回值-1null往往会导致调用者出现错误。

  • 如果错误哨兵将与结果名称空间中的正常值冲突,则几乎可以肯定使用异常,因为必须使用不同的通道来传递该错误。

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

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

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

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