为什么我无法在Python中处理KeyboardInterrupt?

45 投票
7 回答
45788 浏览
提问于 2025-04-16 09:27

我在Windows上写Python 2.6.6的代码,代码大致是这样的:

try:
    dostuff()
except KeyboardInterrupt:
    print "Interrupted!"
except:
    print "Some other exception?"
finally:
    print "cleaning up...."
    print "done."

dostuff()是一个函数,它会一直循环,从输入流中一行一行地读取内容并进行处理。我想在按下Ctrl-C时能够停止这个函数并进行清理。

但是现在发生的情况是,except KeyboardInterrupt:下面的代码根本没有执行。唯一打印出来的就是“正在清理...”,然后出现了一个错误追踪信息,看起来像这样:

Traceback (most recent call last):
  File "filename.py", line 119, in <module>
    print 'cleaning up...'
KeyboardInterrupt

所以,异常处理的代码没有运行,而错误追踪信息显示在finally部分发生了KeyboardInterrupt,这听起来不对,因为按Ctrl-C本来就是让这个部分运行的原因啊!甚至连通用的except:部分也没有执行。

编辑:根据评论的建议,我把try:块里的内容换成了sys.stdin.read()。问题依然如之前描述的那样,finally:块的第一行代码运行了,然后打印出相同的错误追踪信息。

编辑 #2: 如果我在读取之后加上几乎任何东西,处理程序就能正常工作。所以,这段代码失败了:

try:
    sys.stdin.read()
except KeyboardInterrupt:
    ...

但这段代码就能正常工作:

try:
    sys.stdin.read()
    print "Done reading."
except KeyboardInterrupt:
    ...

这是打印出来的内容:

Done reading. Interrupted!
cleaning up...
done.

所以,出于某种原因,“读取完成。”这一行被打印出来,尽管异常发生在前一行。这其实不是问题——显然我需要能够在“try”块的任何地方处理异常。然而,打印的效果不正常——它没有像应该那样在后面换行!“Interrupted”被打印在同一行上……而且前面还有一个空格,真是奇怪……总之,之后代码还是按预期执行了。

我觉得这可能是处理被阻塞的系统调用时的一个错误。

7 个回答

2

我遇到过类似的问题,这是我找到的解决办法:

try:
    some_blocking_io_here() # CTRL-C to interrupt
except:
    try:
        print() # any i/o will get the second KeyboardInterrupt here?
    except:
        real_handler_here()
2

sys.stdin.read() 是一个系统调用,所以在不同的系统上它的表现会有所不同。在 Windows 7 上,我觉得发生的情况是输入被缓冲了,也就是说,当你使用 sys.stdin.read() 时,它会返回所有内容,直到你按下 Ctrl-C。然后一旦你再次访问 sys.stdin,它就会发送“Ctrl-C”的信号。

你可以试试下面的代码,

def foo():
    try:
        print sys.stdin.read()
        print sys.stdin.closed
    except KeyboardInterrupt:
        print "Interrupted!"

这表明 stdin 的缓冲机制正在起作用,这导致再次调用 stdin 时能够识别到键盘输入。

def foo():
    try:
        x=0
        while 1:
            x += 1
        print x
    except KeyboardInterrupt:
        print "Interrupted!"

看起来并没有什么问题。

那么 dostuff() 是不是在从 stdin 读取数据呢?

23

异步异常处理不太可靠(比如信号处理程序引发的异常,或者通过C API在外部上下文中引发的异常等)。如果代码中有一些协调,明确哪个代码块负责捕获这些异常,你就能更有可能正确处理异步异常(在调用栈中尽量高的位置处理,除了非常关键的函数外)。

被调用的函数(dostuff)或者更深层的函数可能会有自己处理KeyboardInterrupt或BaseException的代码,而你可能没有考虑到或者无法处理。

这个简单的例子在Python 2.6.6(x64)和Windows 7(64位)下运行得很好:

>>> import time
>>> def foo():
...     try:
...             time.sleep(100)
...     except KeyboardInterrupt:
...             print "INTERRUPTED!"
...
>>> foo()
INTERRUPTED!  #after pressing ctrl+c

编辑:

经过进一步调查,我尝试了我认为其他人用来重现这个问题的例子。我有点懒,所以省略了“finally”。

>>> def foo():
...     try:
...             sys.stdin.read()
...     except KeyboardInterrupt:
...             print "BLAH"
...
>>> foo()

在按下CTRL+C后,这个代码立即返回。更有趣的是,当我立刻再次调用foo时:

>>> foo()

Traceback (most recent call last):
  File "c:\Python26\lib\encodings\cp437.py", line 14, in decode
    def decode(self,input,errors='strict'):
KeyboardInterrupt

异常立即被引发,而我并没有按CTRL+C。

这似乎是有道理的——看起来我们在处理Python中异步异常的细微差别。实际上,异步异常需要经过几条字节码指令才能真正被弹出并在当前执行上下文中引发。(这是我过去玩这个时看到的行为)

查看C API: http://docs.python.org/c-api/init.html#PyThreadState_SetAsyncExc

这在某种程度上解释了为什么在这个例子中,KeyboardInterrupt会在finally语句执行的上下文中被引发:

>>> def foo():
...     try:
...             sys.stdin.read()
...     except KeyboardInterrupt:
...             print "interrupt"
...     finally:
...             print "FINALLY"
...
>>> foo()
FINALLY
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in foo
KeyboardInterrupt

可能有一些疯狂的自定义信号处理程序与解释器的标准KeyboardInterrupt/CTRL+C处理程序混合在一起,导致了这种行为。例如,read()调用检测到信号并退出,但在注销其处理程序后又重新引发信号。如果不检查解释器的代码库,我也无法确定。

这就是为什么我通常不太喜欢使用异步异常的原因……

编辑 2

我认为有必要提交一个bug报告。

又有更多理论……(只是基于阅读代码)查看文件对象源代码:http://svn.python.org/view/python/branches/release26-maint/Objects/fileobject.c?revision=81277&view=markup

file_read调用Py_UniversalNewlineFread()。fread可能会因为errno = EINTR而返回错误(它自己处理信号)。在这种情况下,Py_UniversalNewlineFread()会退出,但没有使用PyErr_CheckSignals()进行信号检查,因此处理程序无法同步调用。file_read清除了文件错误,但也没有调用PyErr_CheckSignals()。

查看getline()和getline_via_fgets()了解它是如何使用的。这个模式在类似问题的bug报告中有记录:(http://bugs.python.org/issue1195)。所以看起来信号在解释器中被处理的时间是不确定的。

我想深入研究的价值不大,因为仍然不清楚sys.stdin.read()的例子是否是你“dostuff()”函数的正确类比。(可能存在多个bug)

撰写回答