无法在命令提示符中两次捕获 KeyboardInterrupt?

19

今天,我需要在Windows命令提示符下检查我的脚本运行情况[1],突然发现了一些奇怪的东西。我正在处理类似于这个的问题,但是这已经足以展示问题了。以下是代码。

def bing():
    try:
        raw_input()
    except KeyboardInterrupt:
        print 'This is what actually happened here!'

try:                     # pardon me for those weird strings
    bing()               # as it's consistent with everything in the chat room (see below)
    print 'Yoo hoo...'
except KeyboardInterrupt:
    print 'Nothing happens here too!'

这是情况:当脚本运行时,它会等待输入,用户应该按下Ctrl+C来触发KeyboardInterrupt,然后被bing()中的except块捕获。所以,这应该是实际的输出。当我在Ubuntu终端和IDLE(在Windows和Ubuntu上)运行它时,就会发生这种情况。

This is what actually happened here!
Yoo hoo...

但是,在Windows命令提示符上,这并没有按预期进行。我得到了一个奇怪的输出。

This is what actually happened here! Nothing happens here too!

看起来一个KeyboardInterrupt会传播到整个程序并最终终止它。

我尝试了我所能做的一切。首先,我使用了一个signal.signal来处理SIGINT(不起作用),然后我使用处理函数来引发一个稍后会捕获的Exception(这也不起作用),然后事情变得比以前更加复杂了。所以,我回到了我那古老的try...catch。然后,我去了Pythonist之间的房间。

@poke建议,当我们按下Ctrl+C时会引发一个EOFError。然后,@ZeroPiraeus说,当人们按下Ctrl+ZEnter时,会引发EOFError

那很有帮助,在几分钟的摆弄后,就开始了讨论。很快,一切都变成了混乱!有些结果是好的,有些是意外的,还有一些变得混乱不堪!

怪人!

结论是停止使用Windows并让我的朋友们使用终端(我同意)。不过,我可以通过捕获EOFErrorKeyboardInterrupt来进行解决。虽然每次按下Ctrl+ZEnter都感觉很懒,但这对我来说不是一个大问题。但是,这对我来说是一种强迫症。

进一步的研究发现,当我在CMD中按下Ctrl+C时,没有引发KeyboardInterrupt

什么???

底部什么都没有。那么,这里到底发生了什么?为什么KeyboardInterrupt会传播?有没有任何方法可以使输出与终端保持一致?


[1]: 我一直在终端上工作,但今天我需要确保我的脚本在所有平台上都能正常工作(特别是因为我的大多数朋友都不是程序员,只是坚持使用Windows)。


2
这似乎很相关。 - user2357112
1个回答

10
这个问题 user2357112 提供了一些解释:为什么无法在Python中处理KeyboardInterrupt?
键盘中断是异步引发的,因此它不会立即终止应用程序。相反,Ctrl+C 在某种事件循环中被处理,需要一段时间才能到达那里。不幸的是,在这种情况下,您无法可靠地捕获 KeyboardInterrupt。但我们可以采取一些措施来达到目的。
正如我昨天所解释的,停止 raw_input 调用的异常不是 KeyboardInterrupt,而是 EOFError。您可以通过将 bing 函数更改为以下内容轻松验证此内容:
def bing():
    try:
        raw_input()
    except Exception as e:
        print(type(e))

你会看到打印出来的异常类型是EOFError而不是KeyboardInterrupt。你还会发现print甚至没有完全执行:没有新行。这显然是因为输出被中断了,正好在打印语句将异常类型写入stdout之后就收到了中断信号。如果你在打印语句中添加更多内容,也可以看到这一点:
def bing():
    try:
        raw_input()
    except EOFError as e:
        print 'Exception raised:', 'EOF Error'

请注意,此处的print语句使用了两个不同的参数。当我们执行这段代码时,我们可以看到“Exception raised”文本,但“EOF Error”不会出现。相反,外部调用的except将触发并捕获键盘中断。
然而,在Python 3中情况变得有些失控。看看这段代码:
def bing():
    try:
        input()
    except Exception as e:
        print('Exception raised:', type(e))

try:
    bing()
    print('After bing')
except KeyboardInterrupt:
    print('Final KeyboardInterrupt')

这基本上就是我们之前做的事情,只是针对Python 3语法进行了修改。如果我运行这个程序,我会得到以下输出:
Exception raised: <class 'EOFError'>
After bing
Final KeyboardInterrupt

我们可以看到,EOFError被正确捕获,但由于某种原因,Python 3的执行时间比Python 2长得多,在执行bing()后还执行了print。更糟糕的是,在一些cmd.exe执行中,我得到的结果是根本没有捕获到键盘中断(因此,显然,中断在程序已经完成后被处理)。
那么如果我们想确保我们得到一个键盘中断,我们该怎么办呢?有一件事情我们肯定知道,那就是中断input()(或raw_input())提示总是会引发EOFError:这是我们一直看到的唯一一致的事情。所以我们可以捕获它,然后确保我们得到键盘中断。
做到这一点的一种方法是从EOFError的异常处理程序中触发KeyboardInterrupt。但这不仅感觉有点不干净,而且也不能保证中断实际上是什么终止了输入提示(谁知道还可能有什么其他东西会引发EOFError呢?)。因此,我们应该让已经存在的中断信号生成异常。
我们做的方法非常简单:等待。到目前为止,我们的问题是执行继续进行,因为异常没有足够快地到达。那么,在继续其他事情之前,如果我们稍微等待一下让异常最终到达呢?
import time
def bing():
    try:
        input() # or raw_input() for Python 2
    except EOFError:
        time.sleep(1)

try:
    bing()
    print('After bing')
except KeyboardInterrupt:
    print('Final KeyboardInterrupt')

现在,我们只需要捕获EOFError并等待一段时间,让后台的异步进程安定下来,并决定是否中断执行。这样可以始终使我在外部try/catch中捕获KeyboardInterrupt,并且除了异常处理程序中的内容之外不会打印任何其他内容。
你可能担心等待一秒钟太长,但在我们中断执行的情况下,那一秒钟从未真正持续很长时间。在time.sleep后的几毫秒内,中断被捕获,我们进入异常处理程序。因此,这一秒钟只是一个故障保护,足以等待异常及时到达。在最坏的情况下,如果实际上没有中断而只是“正常”的EOFError呢?那么之前“无限阻塞用户输入”的程序将需要再等待一秒钟才能继续;那应该永远不会成为问题(更不用说EOFError可能非常罕见)。
所以,我们有了解决方案:只需捕获EOFError并稍等片刻即可。至少我希望这是一种适用于其他机器的解决方案^_^“经过昨晚的尝试,我对此并不确定——但至少我在所有终端和不同的Python版本上都得到了一致的体验。

1
这确实是一个不错的解决方法。我已经设置好了捕获 EOFError 的一切,现在我添加了等待100毫秒的时间(对我来说足够了),它很好地工作了(多亏了你)。这使我能够在中断终止主程序之前捕获它们。所以,双赢 :) - Waffle's Crazy Peanut
1
PyOS_Readline 试图解决 Windows 控制台控制事件在新线程上到达的问题。请为我尝试以下内容:from ctypes import *; kernel32 = WinDLL('kernel32', use_last_error=True); kernel32.ReadFile(kernel32.GetStdHandle(-10), (c_char * 1)(), 1, byref(c_uint()), None); [CTRL+C] print(get_last_error())。它应该是 ERROR_OPERATION_ABORTED(995)。这是我在 Windows 7 中得到的,但在 Windows 10 中没有设置错误。 - Eryk Sun
@eryksun 很有趣!不过我在Windows 8.1上用Python 2.7和3.4都得到了0 :/ - poke
1
这证实了这不仅仅是Windows 10的一个bug。在Windows 8中,他们重写了客户端IPC到控制台主机(conhost.exe)。控制台API过去使用NT LPC协议和ReadFile用于控制台句柄被路由到ReadConsoleA,这就是设置ERROR_OPERATION_ABORTED的原因。相反,在Windows 8中有了ConDrv内核设备,因此控制台句柄是NT文件对象的常规句柄。控制台文件的ReadFile通过NtReadFile系统调用无异常地处理,这意味着ERROR_OPERATION_ABORTED没有被设置--违反了它的文档规定。 - Eryk Sun
这是一个Windows的bug吗?我怀疑我们不能指望在短时间内有任何改变。既然你知道内部发生了什么,你介意开一个Python bug,以便在未来的Python版本中以不同的方式解决这个问题吗?(也许有一些方法可以解决这个问题) - poke
1
尝试使用kernel32.ReadConsoleA替换kernel32.ReadFile的代码。它们具有相同的调用签名。如果设置了ERROR_OPERATION_ABORTED,那么Python 3的解决方法是使用Drekin的win-unicode-console。这可能会被移植到Python 2。 - Eryk Sun

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