在Python中捕获KeyboardInterrupt而不使用try-except

121

有没有一种方法可以在 Python 中捕获 KeyboardInterrupt 事件,而不必把所有代码放在 try-except 语句中?

如果用户按下 Ctrl+C,我想要干净地退出而不留下任何痕迹。

6个回答

172

是的,您可以使用模块 signal 安装中断处理程序,并使用 threading.Event 永久等待:

import signal
import sys
import time
import threading

def signal_handler(signal, frame):
    print('You pressed Ctrl+C!')
    sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)
print('Press Ctrl+C')
forever = threading.Event()
forever.wait()

11
请注意,信号模块存在一些特定于平台的问题--这不应该影响到这个张贴者,但是“在Windows上,signal()函数只能使用SIGABRT、SIGFPE、SIGILL、SIGINT、SIGSEGV或SIGTERM进行调用。对于其他情况,将引发ValueError错误。” - bgporter
7
也能很好地与线程一起使用。不过,我希望您永远不要使用 while True: continue 这样的代码风格,尽管在那种情况下,while True: pass 更加简洁。使用while True: continue会很浪费资源,推荐使用类似 while True: time.sleep(60 * 60 * 24) 的方式(每次睡眠一天是完全随意的)。 - Chris Morgan
1
如果你正在使用Chris Morgan的建议来使用time(正如你应该做的那样),别忘了要import time :) - Seaux
1
如果我调用sys.exit(0),会触发SystemExit异常。如果您与此结合使用,可以使其正常工作:https://dev59.com/-3VC5IYBdhLWcg3w1Etq#13723190 - leetNightshade
2
你可以使用 signal.pause() 代替重复睡眠。 - Croad Langshan
显示剩余2条评论

44

如果你只是想不显示 traceback,将你的代码改成这样:

## all your app logic here
def main():
   ## whatever your app does.


if __name__ == "__main__":
   try:
      main()
   except KeyboardInterrupt:
      # do nothing here
      pass

(是的,我知道这并没有直接回答问题,但不太清楚为什么需要一个try/except块会引起反对 - 也许这可以减轻提问者的烦恼)


6
出于某些原因,这对我并不总是有效。signal.signal(signal.SIGINT, lambda s, f: sys.exit(0)) 总是有效的。 - Hal Canary
这种方法并不总是适用于使用线程的东西,例如pygtk。有时候^C只会杀死当前线程而不是整个进程,因此异常只会在该线程中传播。 - Sudo Bash
有另一个关于使用pygtk的Ctrl+C的SO问题:https://dev59.com/YmQn5IYBdhLWcg3w_LTx - bgporter
@HalCanary 尝试在 VScode 终端中运行程序。使用 try-except 捕获 Ctrl-C 在某些情况下无法正常工作,但在几乎所有其他环境中都可以正常工作。 - Sulfuric Acid

33

除了设置自己的信号处理程序外,另一种方法是使用上下文管理器来捕获并忽略异常:

>>> class CleanExit(object):
...     def __enter__(self):
...             return self
...     def __exit__(self, exc_type, exc_value, exc_tb):
...             if exc_type is KeyboardInterrupt:
...                     return True
...             return exc_type is None
... 
>>> with CleanExit():
...     input()    #just to test it
... 
>>>

这将去除 try-except 代码块,同时保留一些明确说明正在进行的操作。

这还允许您在代码的某些部分中忽略中断,而无需每次设置和重置信号处理程序。


2
不错,这个解决方案似乎比处理信号更直接地表达了目的。 - Seaux
使用多进程库,我不确定应该在哪个对象上添加这些方法...有什么线索吗? - Stéphane
@Stéphane 你的意思是什么?在处理多进程时,你必须同时处理父进程和子进程中的信号,因为它可能会在两者中触发。这真的取决于你正在做什么以及你的软件将如何使用。 - Bakuriu

9

我知道这是一个老问题,但我先来到这里,然后发现了 atexit 模块。目前我还不知道它的跨平台记录或所有注意事项的完整列表,但到目前为止,正是我在尝试处理Linux上 post-KeyboardInterrupt 清理时所寻找的东西。只是想再提供另一种解决问题的方式。

我想在 Fabric 操作的上下文中进行 post-exit 清理,因此用 try/except 将所有东西都包装起来对我也不是一个选项。我觉得在这种情况下,atexit 可能是一个很好的选择,因为你的代码不处于控制流的最高级别。

atexit 在开箱即用时非常强大且可读,例如:

import atexit

def goodbye():
    print "You are now leaving the Python sector."

atexit.register(goodbye)

你还可以将其用作装饰器(从2.6版本开始;以下示例摘自文档):

import atexit

@atexit.register
def goodbye():
    print "You are now leaving the Python sector."

如果你只想针对KeyboardInterrupt进行设置,那么这个问题的其他回答可能更适合你。但是请注意,atexit模块仅有大约70行代码,创建一个类似的版本并使异常以参数的形式传递给回调函数是很容易的。(需要修改的atexit限制: 目前我无法想象如何让退出回调函数知道有关异常的信息; atexit处理程序捕获异常,调用您的回调函数,然后重新引发该异常。 但是您可以使用不同的方法。) 详见以下链接:

2
atexit在KeyboardInterrupt(Python 3.7)下无法正常工作。 - TimZaman
在这里使用 KeyboardInterrupt 工作了(Python 3.7,MacOS)。也许是平台特定的小问题? - Niko Nyman
可以确认 atexit 在 MacOS 和 Ubuntu 18.04 上对于 Python 3.7 和 3.8 都有效。 - Ainz Titor

4

您可以通过替换sys.excepthook来防止打印KeyboardInterrupt的堆栈跟踪,而不需要使用try: ... except KeyboardInterrupt: pass(这是最明显和可能是“最好”的解决方案,但您已经知道它并且要求其他方法)。示例代码如下:

def custom_excepthook(type, value, traceback):
    if type is KeyboardInterrupt:
        return # do nothing
    else:
        sys.__excepthook__(type, value, traceback)

如果用户按下Ctrl-C,我希望能够干净地退出而不留下任何痕迹。 - Alex
8
这完全不正确。KeyboardInterrupt异常是在中断处理程序期间创建的。SIGINT的默认处理程序会引发KeyboardInterrupt异常,因此,如果您不想要该行为,只需为SIGINT提供另一个信号处理程序即可。您是正确的,异常只能在try/except中处理,但在这种情况下,您可以防止异常首先被引发。 - Matt
1
是的,我发帖后大约三分钟就学会了这一点,当kotlinski的答案出现时。 ;) - user395760

4

我试着使用大家提供的解决方案,但最终只能自己动手修改代码才使其正常工作。以下是我的修改后的代码:

import signal
import sys
import time

def signal_handler(signal, frame):
    print('You pressed Ctrl+C!')
    print(signal) # Value is 2 for CTRL + C
    print(frame) # Where your execution of program is at moment - the Line Number
    sys.exit(0)

#Assign Handler Function
signal.signal(signal.SIGINT, signal_handler)

# Simple Time Loop of 5 Seconds
secondsCount = 5
print('Press Ctrl+C in next '+str(secondsCount))
timeLoopRun = True 
while timeLoopRun:  
    time.sleep(1)
    if secondsCount < 1:
        timeLoopRun = False
    print('Closing in '+ str(secondsCount)+ ' seconds')
    secondsCount = secondsCount - 1

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