【问题标题】:in python, is it possible for an exception to occur after a call but "before" the try block that follows it?在 python 中,是否有可能在调用之后但“之前”跟随它的 try 块发生异常?
【发布时间】:2019-05-07 22:31:19
【问题描述】:

给定一个函数调用和紧随其后的 try 块,是否存在调用正常返回但引发异常且未被 try 块捕获的情况?

例如:

# example 1
resource = acquire_a_resource()
try:
    resource.do_something()
    # some more code...
finally:
    resource.close()

有没有可能acquire_a_resource()正常返回但resource.close()不会被调用?

或者换句话说,是否有任何场景:

# example 2
resource = None
try:
    resource = acquire_a_resource()
    resource.do_something()
    # some more code...
finally:
    if resource:
        resource.close()

会比示例 #1 更安全吗?

也许是因为与KeyboardInterrupt/threads/signals 有关?

【问题讨论】:

    标签: python multithreading exception signals keyboardinterrupt


    【解决方案1】:

    是的,至少在理论上是这样,尽管在 CPython 中不是(详见脚注)。线程不是特别相关,但您的 KeyboardInterrupt 场景恰到好处:

    resource = acquire_a_resource()
    

    调用函数。函数获取资源并返回句柄,然后在赋值给变量的过程中,1发生键盘中断。所以:

    try:
    

    不运行 - 而是发生 KeyboardInterrupt 异常,离开当前函数并取消绑定变量。

    第二个版本通过finally 子句,因此假设if resource 发现它是布尔值,resource.close() 确实会被调用。

    (请注意,实际上触发它通常非常困难:您必须恰到好处地安排中断时间。您可以通过例如在try 之前添加time.sleep(1) 来增加竞争窗口。)

    在许多情况下,with 语句效果很好:

    with acquire_a_resource() as resource:
        resource.do_something()
    

    close 内置于 __exit__ 方法中。即使该块通过异常转义,该方法也会运行。


    1一般情况下,实现有义务完成获取到的资源到变量的绑定,否则会出现不可恢复的竞态。在 CPython 中,这是因为解释器会检查语句之间的中断,有时还会检查源代码中的关键位置。

    CPython 实际上增加了另一个特殊情况:

        /* Do periodic things.  Doing this every time through
           the loop would add too much overhead, so we do it
           only every Nth instruction.  We also do it if
           ``pendingcalls_to_do'' is set, i.e. when an asynchronous
           event needs attention (e.g. a signal handler or
           async I/O handler); see Py_AddPendingCall() and
           Py_MakePendingCalls() above. */
    
        if (_Py_atomic_load_relaxed(&_PyRuntime.ceval.eval_breaker)) {
            opcode = _Py_OPCODE(*next_instr);
            if (opcode == SETUP_FINALLY ||
                opcode == SETUP_WITH ||
                opcode == BEFORE_ASYNC_WITH ||
                opcode == YIELD_FROM) {
                /* Few cases where we skip running signal handlers and other
                   pending calls:
                   - If we're about to enter the 'with:'. It will prevent
                     emitting a resource warning in the common idiom
                     'with open(path) as file:'.
                   - If we're about to enter the 'async with:'.
                   - If we're about to enter the 'try:' of a try/finally (not
                     *very* useful, but might help in some cases and it's
                     traditional)
                   - If we're resuming a chain of nested 'yield from' or
                     'await' calls, then each frame is parked with YIELD_FROM
                     as its next opcode. If the user hit control-C we want to
                     wait until we've reached the innermost frame before
                     running the signal handler and raising KeyboardInterrupt
                     (see bpo-30039).
                */
                goto fast_next_opcode;
            }
    

    Python/ceval.c,1000 行附近)。

    所以实际上try确实 运行,因为这里有一个SETUP_FINALLY。我完全不清楚其他 Python 实现是否做同样的事情。

    【讨论】:

    • 我很确定with 也有同样的中断问题。
    • with 捕获资源在类对象中(稍后__enter__ 返回时resource 绑定到该资源)。 __exit__ 使用类对象的资源副本进行关闭。这确实意味着资源管理器本身必须仔细编码:构造没有资源绑定的类,然后在一次操作中获取并绑定。实现中有一定程度的“笨拙”:您必须假设var = acquire() 本身是原子的,即“信号发生,引发异常”发生在绑定之前或之后,而不是在绑定期间(但就是这样)。
    • 即使使用with,KeyboardInterrupt 也可能在acquire_a_resource() 成功和with 块设置之间到达(这忽略了未询问但更危险的可能性KeyboardInterrupt 到达期间 acquire_a_resource,或在__exit__finally 块期间)。 KeyboardInterrupt 非常讨厌。如果不安装您自己的 SIGINT 处理程序,我认为没有真正安全的方法来处理它。
    • 如果acquire_a_resource 本身是非原子的,你要么推迟^C,要么编写漂亮的英雄代码,是的。
    • 顺便说一句,我一直认为 Python 应该具有延迟键盘中断(和一般信号)的功能,以便您可以编写:with defer(...): code 其中... 掩码中的任何信号/set/list/whatever 一直保留到块退出。
    猜你喜欢
    • 1970-01-01
    • 2010-09-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多