对于 Python 中任何可能的 try-finally 块,是否保证始终执行finally
块?
例如,假设我在except
块中返回:
try:
1/0
except ZeroDivisionError:
return
finally:
print("Does this code run?")
或者也许我重新提出一个Exception
:
try:
1/0
except ZeroDivisionError:
raise
finally:
print("What about this code?")
测试表明,对于上述示例,finally
确实被执行,但我想还有其他我没有想到的场景。
是否有任何finally
块无法在 Python 中执行的情况?
保证"是一个比任何finally
实现都应得的更强大的词。可以保证的是,如果执行从整个try
-finally
构造中流出,它将通过finally
来执行此操作。不能保证的是执行会流出try
-finally
.
如果生成器或异步协程中的
finally
从不执行到结论,则可能永远不会运行。有很多方法可能发生;这是其中之一:def gen(text): try: for line in text: try: yield int(line) except: # Ignore blank lines - but catch too much! pass finally: print('Doing important cleanup') text = ['1', '', '2', '', '3'] if any(n > 1 for n in gen(text)): print('Found a number') print('Oops, no cleanup.')
请注意,这个例子有点棘手:当生成器被垃圾回收时,Python 试图通过抛出一个
GeneratorExit
异常来运行finally
块,但在这里我们捕获该异常,然后再次yield
,此时 Python 会打印一个警告("生成器忽略了 GeneratorExit")并放弃。有关详细信息,请参阅 PEP 342(通过增强型生成器的协程)。生成器或协程可能无法执行以得出结论的其他方式包括如果对象从未被 GC'ed(是的,这是可能的,即使在 CPython 中),或者如果
async with
await
在__aexit__
中 s,或者如果对象在finally
块中await
s 或yield
s。此列表并非详尽无遗。如果所有非守护程序线程首先退出,则守护程序线程中的
finally
可能永远不会执行。os._exit
将立即停止进程,而不执行finally
块。os.fork
可能会导致finally
块执行两次。除了您期望发生两次的事情的正常问题外,如果对共享资源的访问未正确同步,这可能会导致并发访问冲突(崩溃、停顿等)。由于
multiprocessing
在使用forkstart方法(Unix上的默认值)时使用fork-without-exec来创建工作进程,然后在worker的工作完成后在worker中调用os._exit
,因此finally
和multiprocessing
交互可能会出现问题(示例)。- C 级分段错误将阻止
finally
块运行。 kill -SIGKILL
将阻止finally
块运行。SIGTERM
和SIGHUP
还将阻止finally
块运行,除非您安装处理程序来自己控制关闭;默认情况下,Python 不处理SIGTERM
或SIGHUP
。finally
中的异常可能会阻止清理完成。一个特别值得注意的情况是,如果用户在我们开始执行finally
块时点击control-C。Python 将引发一个KeyboardInterrupt
并跳过finally
块内容的每一行。(KeyboardInterrupt
-safe 代码很难编写)。- 如果计算机断电,或者休眠且未唤醒,则
finally
块将不会运行。
finally
块不是一个交易系统;它不提供原子性保证或类似的东西。其中一些例子可能看起来很明显,但很容易忘记这样的事情可能发生,并过于依赖finally
。
是的。最后总是赢。
击败它的唯一方法是在finally:
有机会执行之前停止执行(例如,使解释器崩溃,关闭计算机,永久挂起生成器)。
我想还有其他我没有想到的场景。
以下是您可能没有想到的更多内容:
def foo():
# finally always wins
try:
return 1
finally:
return 2
def bar():
# even if he has to eat an unhandled exception, finally wins
try:
raise Exception('boom')
finally:
return 'no boom'
根据您退出口译员的方式,有时您可以最终"取消",但不是这样:
>>> import sys
>>> try:
... sys.exit()
... finally:
... print('finally wins!')
...
finally wins!
$
使用不稳定的os._exit
(在我看来,这属于"使解释器崩溃"):
>>> import os
>>> try:
... os._exit(1)
... finally:
... print('finally!')
...
$
我目前正在运行这段代码,以测试在宇宙热寂后是否最终仍会执行:
try:
while True:
sleep(1)
finally:
print('done')
但是,我仍在等待结果,所以稍后再回来查看。
根据 Python 文档:
无论之前发生了什么,一旦代码块完成并处理任何引发的异常,就会执行最终块。即使异常处理程序或 else-block 中存在错误并引发新的异常,最终块中的代码仍会运行。
还应该注意的是,如果有多个返回语句,包括 finally 块中的一个,那么 finally 块返回是唯一将执行的语句。
嗯,是也不是。
可以保证的是,Python 将始终尝试执行 finally 块。如果您从块返回或引发未捕获的异常,则在实际返回或引发异常之前执行 finally 块。
(您可以通过简单地运行问题中的代码来控制自己)
我能想象到的唯一情况是当 Python 解释器本身崩溃时,例如在 C 代码中或由于断电。
我在不使用生成器函数的情况下找到了这个:
import multiprocessing
import time
def fun(arg):
try:
print("tried " + str(arg))
time.sleep(arg)
finally:
print("finally cleaned up " + str(arg))
return foo
list = [1, 2, 3]
multiprocessing.Pool().map(fun, list)
睡眠可以是可能运行时间不一致的任何代码。
这里似乎发生的事情是,第一个完成的并行进程成功离开 try 块,但随后尝试从函数返回一个尚未在任何地方定义的值 (foo),这会导致异常。该异常会杀死映射,而不允许其他进程到达其最终块。
此外,如果您在 try 块中的 sleep() 调用之后添加行bar = bazz
。然后到达该行的第一个进程抛出异常(因为未定义 bazz),这会导致运行它自己的 finally 块,但随后杀死映射,导致其他 try 块消失而不到达它们的 finally 块,第一个进程也没有到达它的 return 语句。
对于 Python 多处理来说,这意味着你不能信任异常处理机制来清理所有进程中的资源,即使其中一个进程也可能有异常。需要额外的信号处理或管理多处理映射调用之外的资源。
您可以使用带有 if 语句的 finally 语句,下面的示例是检查网络连接,如果已连接,它将运行 finally 块
try:
reader1, writer1 = loop.run_until_complete(self.init_socket(loop))
x = 'connected'
except:
print("can't connect server transfer") #open popup
x = 'failed'
finally :
if x == 'connected':
with open('text_file1.txt', "r") as f:
file_lines = eval(str(f.read()))
else:
print("not connected")