如何在Python中捕获SIGINT信号?

698
我正在编写一个Python脚本,它启动了多个进程和数据库连接。偶尔我想用Ctrl+C信号来终止脚本,并且我想进行一些清理工作。
在Perl中,我会这样做:
$SIG{'INT'} = 'exit_gracefully';

sub exit_gracefully {
    print "Caught ^C \n";
    exit (0);
}

如何在Python中实现这个的类似功能?
13个回答

1029
使用以下方式将您的处理程序注册到signal.signal中:
#!/usr/bin/env python
import signal
import sys

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

signal.signal(signal.SIGINT, signal_handler)
print('Press Ctrl+C')
signal.pause()

代码改编自这里
有关signal的更多文档可以在这里找到。

21
为什么要使用这个,而不是使用 KeyboardInterrupt 异常?使用后者难道不更直观吗? - noio
60
Noio提到了两个原因。首先,SIGINT可以通过多种方式发送到您的进程(例如,“kill -s INT <pid>”);我不确定KeyboardInterruptException是否实现为SIGINT处理程序,或者它只能捕获Ctrl+C按键,但无论哪种方式,使用信号处理程序可以使您的意图明确(至少,如果您的意图与OP相同)。更重要的是,使用信号,您不必在每个语句块周围添加try-catch语句才能使它们起作用,这可能在应用程序结构上具有更多或更少的组合性和一般软件工程优势。 - Matt J
56
为什么要捕获信号而不是捕获异常?举个例子,假设你运行一个程序并将输出重定向到日志文件 ./program.py > output.log。当你按下 Ctrl-C 键时,希望程序能够正常退出,并记录所有数据文件已经被刷新并标记为干净,以确认它们处于已知的良好状态。但是,Ctrl-C 会向管道中的所有进程发送 SIGINT 信号,因此 shell 可能会在 program.py 完成打印最终日志之前关闭 STDOUT(现在是 "output.log")。Python 将会抱怨,“close failed in file object destructor: Error in sys.excepthook:”。因此,你需要捕获信号来实现优雅的程序退出。 - Noah Spurrier
27
注意,signal.pause() 在 Windows 上不可用。http://docs.python.org/dev/library/signal.html - May Oakes
17
使用 signal.pause() 的话就会被扣掉 1 只独角兽,因为这会阻塞调用而使得我无法做一些真正的工作 ;)。 - Nick T
显示剩余11条评论

212

你可以像处理其他异常 (KeyboardInterrupt) 一样来处理它。创建一个新文件,将以下内容复制到文件中并从shell中运行该文件,以了解我的意思:

import time, sys

x = 1
while True:
    try:
        print x
        time.sleep(.3)
        x += 1
    except KeyboardInterrupt:
        print "Bye"
        sys.exit()

33
在使用此解决方案时要注意。在捕获 KeyboardInterrupt 异常之前,您应该使用此代码:signal.signal(signal.SIGINT, signal.default_int_handler),否则可能会失败,因为 KeyboardInterrupt 并不总是在应该发生的情况下触发!详细信息请参见 here - Velda
这对于 strg+d 不起作用吗?即使在信号代码行之前? - Asara
KeyboardInterrupt的问题在于它会将您移出try块,这可能并不总是优雅的。考虑长时间运行的循环。signal.signal可以给您更多的控制。 - Shiplu Mokaddim

82

作为上下文管理器:

import signal

class GracefulInterruptHandler(object):

    def __init__(self, sig=signal.SIGINT):
        self.sig = sig

    def __enter__(self):

        self.interrupted = False
        self.released = False

        self.original_handler = signal.getsignal(self.sig)

        def handler(signum, frame):
            self.release()
            self.interrupted = True

        signal.signal(self.sig, handler)

        return self

    def __exit__(self, type, value, tb):
        self.release()

    def release(self):

        if self.released:
            return False

        signal.signal(self.sig, self.original_handler)

        self.released = True

        return True

使用方法:

with GracefulInterruptHandler() as h:
    for i in xrange(1000):
        print "..."
        time.sleep(1)
        if h.interrupted:
            print "interrupted!"
            time.sleep(2)
            break

嵌套处理程序:

with GracefulInterruptHandler() as h1:
    while True:
        print "(1)..."
        time.sleep(1)
        with GracefulInterruptHandler() as h2:
            while True:
                print "\t(2)..."
                time.sleep(1)
                if h2.interrupted:
                    print "\t(2) interrupted!"
                    time.sleep(2)
                    break
        if h1.interrupted:
            print "(1) interrupted!"
            time.sleep(2)
            break

From here: https://gist.github.com/2907502


当按下ctrl-C时,它也可以抛出StopIteration来中断最内层的循环,对吗? - Theo Belaire
@TheoBelaire 我会创建一个生成器,接受可迭代对象作为参数并注册释放信号处理程序,而不仅仅是抛出 StopIteration。 - Udi

33

通过捕获 KeyboardInterrupt 异常,您可以处理 CTRL+C。您可以在异常处理程序中实现任何清理代码。


32

又一个代码片段

main指代为主函数,exit_gracefully指代Ctrl+C处理程序

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass
    finally:
        exit_gracefully()

8
你只应该在不应该发生的情况下使用"except"。 在这种情况下,KeyboardInterrupt是预期会发生的,因此使用这种结构并不好。 - Tristan
26
在其他语言中,是的,但在Python中,异常不仅仅用于那些不应该发生的事情。实际上,在Python中,使用异常来进行流程控制(适当时)被认为是一种良好的编程风格。 - Ian Goldby
2
为什么不在except中使用exit_gracefully()而是传递并添加finally - Break
6
在最终结束时打破它,这样main()完成后也将调用exit_gracefully() - Jossef Harush Kadouri

30

根据Python的文档

import signal
import time

def handler(signum, frame):
    print 'Here you go'

signal.signal(signal.SIGINT, handler)

time.sleep(10) # Press Ctrl+c here

13

如果您想确保清理过程完成,我建议在Matt J's answer的基础上使用SIG_IGN,这样进一步的SIGINT将被忽略,从而防止您的清理被中断。

import signal
import sys

def signal_handler(signum, frame):
    signal.signal(signum, signal.SIG_IGN) # ignore additional signals
    cleanup() # give your process a chance to clean up
    sys.exit(0)

signal.signal(signal.SIGINT, signal_handler) # register the signal with the signal handler first
do_stuff()

谢谢。这是明智的做法,通过清晰地注册信号处理程序来完成。 - undefined

12

我根据@udi的代码进行了修改,使其支持多个信号(没什么花头):

class GracefulInterruptHandler(object):
    def __init__(self, signals=(signal.SIGINT, signal.SIGTERM)):
        self.signals = signals
        self.original_handlers = {}

    def __enter__(self):
        self.interrupted = False
        self.released = False

        for sig in self.signals:
            self.original_handlers[sig] = signal.getsignal(sig)
            signal.signal(sig, self.handler)

        return self

    def handler(self, signum, frame):
        self.release()
        self.interrupted = True

    def __exit__(self, type, value, tb):
        self.release()

    def release(self):
        if self.released:
            return False

        for sig in self.signals:
            signal.signal(sig, self.original_handlers[sig])

        self.released = True
        return True

该代码支持键盘中断调用 (SIGINT) 和 SIGTERM (kill <process>)。


你尝试使用 SIGTERM 吗?似乎 SIGTERM 的原始处理程序不可调用。 - 1' OR 1 --
我在处理程序中添加了一个打印注释,并获得了SIGINT和SIGTERM的事件。在条件语句(if h.interupted:)中添加了一个打印,也被显示出来了。所以我认为它是有效的。 - Cyril N.

10

Matt J的回答相反,我使用一个简单的对象。这使我能够将此处理程序解析到需要安全停止的所有线程中。

class SIGINT_handler():
    def __init__(self):
        self.SIGINT = False

    def signal_handler(self, signal, frame):
        print('You pressed Ctrl+C!')
        self.SIGINT = True


handler = SIGINT_handler()
signal.signal(signal.SIGINT, handler.signal_handler)

其他地方

while True:
    # task
    if handler.SIGINT:
        break

你应该使用事件或 time.sleep(),而不是在变量上进行忙等待循环。 - OlivierM
1
@OlivierM 这真的是应用程序特定的,绝对不是这个示例的重点。例如,阻塞调用或等待函数不会使CPU保持繁忙状态。此外,这只是展示如何完成事情的一个示例。像其他答案中提到的那样,KeyboardInterrupts通常已经足够了。 - Thomas Devoogdt
@ThomasDevoogdt 我喜欢这个解决方案的优雅性 - 我完全理解这里发生了什么。您能否评论为什么这比其他解决方案更好(或更差)?特别是,我发现Udi / Cyril N.的解决方案非常完整,但要复杂得多。 - Aaron Ciuffo
我不喜欢称这个解决方案比另一个更优越。好的开发人员会查看不同的答案,并从可用的建议中提炼出自己的应用程序特定解决方案。但是为了给出一个优点,它非常简单易用和实现。一个相当大的缺点是需要在处理线程中使用非阻塞循环。 - Thomas Devoogdt

4
你可以使用Python内置的信号模块来设置Python中的信号处理程序。具体而言,signal.signal(signalnum, handler)函数用于为信号signalnum注册handler函数作为处理程序。请参考信号模块文档了解更多信息。

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