在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

当前回答

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

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

try-except-else存在的原因是什么? try-except-else模式,或者一般的Python,是否鼓励在流控制中使用异常? 什么时候使用异常呢?

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

这个问题可以从战术的角度来回答。当然有理由去尝试……存在。这里唯一的新添加是else…子句,它的有用性归结为它的独特性:

It runs an extra code block ONLY WHEN there was no exception happened in the try... block. It runs that extra code block, OUTSIDE of the try... block (meaning any potential exceptions happen inside the else... block would NOT be caught). It runs that extra code block BEFORE the final... finalization. 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() In the example above, you can't move that successful log line into behind the finally... block. You can't quite move it into inside the try... block, either, due to the potential exception inside the else... block.

问题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:在什么情况下,应该将程序设计为发出异常?

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

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

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

Their "blast radius" are different. Return value is only available to the immediate caller; exception can be automatically relayed for unlimited distance until it is caught. Their distribution patterns are different. Return value is by definition one piece of data (even though you could return a compound data type such as a dictionary or a container object, it is still technically one value). The exception mechanism, on the contrary, allows multiple values (one at a time) to be returned via their respective dedicate channel. Here, each except FooError: ... and except BarError: ... block is considered as its own dedicate channel.

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

All normal cases should better be returned via return value, because the callers would most likely need to use that return value immediately. The return-value approach also allows nesting layers of callers in a functional programming style. The exception mechanism's long blast radius and multiple channels do not help here. For example, it would be unintuitive if any function named get_something(...) produces its happy path result as an exception. (This is not really a contrived example. There is one practice to implement BinaryTree.Search(value) to use exception to ship the value back in the middle of a deep recursion.) If the caller would likely forget to handle the error sentinel from the return value, it is probably a good idea to use exception's characterist #2 to save caller from its hidden bug. A typical non-example would be the position = find_string(haystack, needle), unfortunately its return value of -1 or null would tend to cause a bug in the caller. If the error sentinel would collide with a normal value in the result namespace, it is almost certain to use an exception, because you'd have to use a different channel to convey that error. If the normal channel i.e. the return value is already used in the happy-path, AND the happy-path does NOT have sophisicated flow control, you have no choice but to use exception for flow control. People keep talking about how Python uses StopIteration exception for iteration termination, and use it to kind of justify "using exception for flow control". But IMHO this is only a practical choice in a particular situation, it does not generalize and glorify "using exception for flow control".

At this point, if you already make a sound decision on whether your function get_stock_price() would produce only return-value or also raise exceptions, or if that function is provided by an existing library so that its behavior has long be decided, you do not have much choice in writing its caller calculate_market_trend(). Whether to use get_stock_price()'s exception to control the flow in your calculate_market_trend() is merely a matter of whether your business logic requires you to do so. If yes, do it; otherwise, let the exception bubble up to a higher level (this utilizes the characteristic #1 "long blast radius" of exception).

In particular, if you are implementing a middle-layer library Foo and you happen to be making a dependency on lower-level library Bar, you would probably want to hide your implementation detail, by catching all Bar.ThisError, Bar.ThatError, ..., and map them into Foo.GenericError. In this case, the long blast radius is actually working against us, so you might hope "only if library Bar were returning its errors via return values". But then again, that decision has long been made in Bar, so you can just live with it.

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

其他回答

Python不赞同异常只用于异常情况的想法,事实上,习惯用法是“请求原谅,而不是许可”。这意味着使用异常作为流控制的常规部分是完全可以接受的,实际上是被鼓励的。

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

假设您有这样一种情况,需要处理一些用户输入,但默认值已经处理完毕。尝试:……除了:…其他:…结构使得代码可读性非常强:

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

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

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

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

答案是,这取决于上下文。如果你这样做:

d = dict()
try:
    item = d['item']
except KeyError:
    item = 'default'

这说明您不是很了解Python。这个功能封装在字典中。获得方法:

item = d.get('item', 'default')

try/except块是一种视觉上更加混乱和冗长的方式,它可以用原子方法在一行中有效地执行。在其他情况下也是如此。

然而,这并不意味着我们应该避免所有的异常处理。在某些情况下,最好避免竞争条件。不要检查文件是否存在,只是尝试打开它,并捕获相应的IOError。出于简单性和可读性的考虑,请尝试将其封装或适当地提取出来。

阅读Python的禅宗,了解有一些原则处于紧张状态,并警惕过分依赖其中任何一条陈述的教条。

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

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

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

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

try-except-else存在的原因是什么? try-except-else模式,或者一般的Python,是否鼓励在流控制中使用异常? 什么时候使用异常呢?

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

这个问题可以从战术的角度来回答。当然有理由去尝试……存在。这里唯一的新添加是else…子句,它的有用性归结为它的独特性:

It runs an extra code block ONLY WHEN there was no exception happened in the try... block. It runs that extra code block, OUTSIDE of the try... block (meaning any potential exceptions happen inside the else... block would NOT be caught). It runs that extra code block BEFORE the final... finalization. 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() In the example above, you can't move that successful log line into behind the finally... block. You can't quite move it into inside the try... block, either, due to the potential exception inside the else... block.

问题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:在什么情况下,应该将程序设计为发出异常?

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

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

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

Their "blast radius" are different. Return value is only available to the immediate caller; exception can be automatically relayed for unlimited distance until it is caught. Their distribution patterns are different. Return value is by definition one piece of data (even though you could return a compound data type such as a dictionary or a container object, it is still technically one value). The exception mechanism, on the contrary, allows multiple values (one at a time) to be returned via their respective dedicate channel. Here, each except FooError: ... and except BarError: ... block is considered as its own dedicate channel.

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

All normal cases should better be returned via return value, because the callers would most likely need to use that return value immediately. The return-value approach also allows nesting layers of callers in a functional programming style. The exception mechanism's long blast radius and multiple channels do not help here. For example, it would be unintuitive if any function named get_something(...) produces its happy path result as an exception. (This is not really a contrived example. There is one practice to implement BinaryTree.Search(value) to use exception to ship the value back in the middle of a deep recursion.) If the caller would likely forget to handle the error sentinel from the return value, it is probably a good idea to use exception's characterist #2 to save caller from its hidden bug. A typical non-example would be the position = find_string(haystack, needle), unfortunately its return value of -1 or null would tend to cause a bug in the caller. If the error sentinel would collide with a normal value in the result namespace, it is almost certain to use an exception, because you'd have to use a different channel to convey that error. If the normal channel i.e. the return value is already used in the happy-path, AND the happy-path does NOT have sophisicated flow control, you have no choice but to use exception for flow control. People keep talking about how Python uses StopIteration exception for iteration termination, and use it to kind of justify "using exception for flow control". But IMHO this is only a practical choice in a particular situation, it does not generalize and glorify "using exception for flow control".

At this point, if you already make a sound decision on whether your function get_stock_price() would produce only return-value or also raise exceptions, or if that function is provided by an existing library so that its behavior has long be decided, you do not have much choice in writing its caller calculate_market_trend(). Whether to use get_stock_price()'s exception to control the flow in your calculate_market_trend() is merely a matter of whether your business logic requires you to do so. If yes, do it; otherwise, let the exception bubble up to a higher level (this utilizes the characteristic #1 "long blast radius" of exception).

In particular, if you are implementing a middle-layer library Foo and you happen to be making a dependency on lower-level library Bar, you would probably want to hide your implementation detail, by catching all Bar.ThisError, Bar.ThatError, ..., and map them into Foo.GenericError. In this case, the long blast radius is actually working against us, so you might hope "only if library Bar were returning its errors via return values". But then again, that decision has long been made in Bar, so you can just live with it.

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