Python中的异常屏蔽?

4

通常使用with语句打开文件,以避免文件句柄泄漏:

with open("myfile") as f:
    …

但是如果异常出现在open调用之间怎么办?在Python解释器中,open函数很可能不是一个原子指令,因此完全有可能在系统调用已经完成之后,open调用还没有完成时发生异步异常,比如KeyboardInterrupt

传统处理方式(例如在POSIX信号中)是使用掩蔽机制:在被掩蔽的情况下,异常的传递被暂停直到它们后来被取消掩蔽。这样可以实现类似于open的操作具有原子性。Python中是否存在这样的原语呢?


[*] 有人可能会说对于KeyboardInterrupt来说并不重要,因为程序即将死亡,但并非所有程序都是如此。一个程序可能选择在顶层上捕获KeyboardInterrupt并继续执行,在这种情况下,泄漏的文件句柄可能随着时间的推移而增加。


1
这是关于异常还是信号的问题? - Paul
责任在于 open 要么成功,要么失败并整理好。你无需担心。 - David Heffernan
@DavidHeffernan:这对于“打开”可能很好,但这并没有真正回答如何以异常安全的方式实现类似操作的问题。 - Rufflewind
1
你正在想象一个不存在的问题。 - David Heffernan
2个回答

1
我认为不可能掩盖exceptions,你可以掩盖signals,但无法掩盖exceptions。在您的情况下,当引发signal.SIGINT(即Ctrl + C)时,会引发KeyboardInterrupt异常。

不可能掩盖Exceptions,因为这没有意义,对吧?假设您正在执行open('file','r'),但是file不存在,这将导致open函数抛出IOError异常,我们不应该能够掩盖这些类型的异常。在上述情况下,由于文件不存在,因此无法完成打开操作,因此掩盖它是没有意义的。

exceptions - 需要特殊处理的异常或异常情况

对于KeyboardInterrupt异常,情况有所不同,因为正如我所说,实际上是一个signal引起了KeyboardInterrupt异常的发生。

从Python 3.3开始,您只能在Unix中使用函数signal.pthread_sigmask [参考文献]屏蔽信号。

为此,您需要将上下文表达式移动到不同的块中,以便我们可以执行类似于屏蔽信号、运行上下文表达式以获取上下文管理器,然后取消屏蔽信号的操作,示例代码如下(请注意,我个人未测试此代码):

import signal
signal.pthread_sigmask(signal.SIG_BLOCK,[signal.SIGINT])
with <context expression> as variable:  # in your case ,open('filename','r')

    signal.pthread_sigmask(signal.SIG_UNBLOCK,[signal.SIGINT])
...

虽然屏蔽同步异常并没有太大的用处,但屏蔽异步异常是非常明智的,因为它们可以在代码中的任何地方发生。屏蔽提供了一种机制,以在需要原子性的关键部分暂停它们。(当然,屏蔽信号总是可选的,但这不是可移植的。)另外,你的例子是错误的: 取消屏蔽需要在 with 之后进行,否则信号将在上下文管理器到达 with 之前被传递。 - Rufflewind
你的意思是说解除掩码需要在完成块之后发生吗?我认为要求是在上下文管理器创建部分期间解除信号的掩码,因此我的示例是正确的,但如果掩码是针对完整的with块,则需要在with块之后进行解除掩码。 - Anand S Kumar
另外,您能否举一个异步异常的例子? - Anand S Kumar
解除掩码需要在块开始时发生。如果在块开始之前发生,那么信号就会被传递,因此上下文管理器的__exit__将永远不会被执行。 - Rufflewind
话虽如此,KeyboardInterrupt 似乎是个例外。Python 默认情况下似乎不会异步使用异常,除非用户或某个库发明了自己的方法,因此也许有一种解决方法…… - Rufflewind
显示剩余2条评论

0
一些澄清:似乎在Python中不常使用异步异常。据我所知,标准库只记录了KeyboardInterrupt。其他库可以通过信号处理程序实现自己的异常,但我认为(或希望?)这不是一个常见的做法,因为异步异常非常棘手。
这里有一个天真的解决方案,它不会起作用:
try:
    handle = acquire_resource(…)
except BaseException as e:
    handle.release()
    raise e
else:
    with handle:
        
  • 异常处理部分仍然容易出现异常:在捕获异常之后但在release完成之前,可能会再次发生KeyboardInterrupt

  • try语句结束和with语句开始之间还存在一个“间隙”,可能会出现异常。

我认为没有办法让它以这种方式工作。

从不同的角度思考,似乎异步异常唯一可能出现的方式是通过信号。如果这是真的,可以像@AnandSKumar建议的那样屏蔽它们。然而,屏蔽不是可移植的,因为它需要pthread。

尽管如此,我们可以通过一些诡计来伪造屏蔽:

def masking_handler(queue, prev_handler):
    def handler(*args):
        queue.append(lambda: prev_handler[0](*args))
    return handler

mask_stack = []

def mask():
    queue = []
    prev_handler = []
    new_handler = masking_handler(queue, prev_handler)
    # swap out the signal handler with our own
    prev_handler[0] = signal.signal(signal.SIGINT, new_handler)
    mask_stack.append((prev_handler[0], queue))

def unmask():
    prev_handler, queue = mask_stack.pop()
    # restore the original signal handler
    signal.signal(signal.SIGINT, prev_handler)
    # the remaining part may never occur if a signal happens right now
    # but that's fine
    for event in queue:
        event()

mask()
with acquire_resource(…) as handle:
    unmask()
    …

如果我们只关心SIGINT,那么这将起作用。不幸的是,对于多个信号,它会崩溃,不仅因为我们不知道哪些信号正在被处理,而且因为我们无法原子地交换多个信号!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接