【问题标题】:How do I capture SIGINT in Python?如何在 Python 中捕获 SIGINT?
【发布时间】:2010-11-09 21:15:57
【问题描述】:

我正在编写一个启动多个进程和数据库连接的 python 脚本。时不时我想用 Ctrl+C 信号杀死脚本,我想做一些清理工作。

在 Perl 中我会这样做:

$SIG{'INT'} = 'exit_gracefully';

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

如何在 Python 中进行类似的操作?

【问题讨论】:

    标签: python controls signals


    【解决方案1】:

    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()
    

    代码改编自here

    更多关于signal 的文档可以在here 找到。

    【讨论】:

    • 您能告诉我为什么要使用它而不是 KeyboardInterrupt 异常吗?使用起来不是更直观吗?
    • Noio:两个原因。首先,可以通过多种方式将 SIGINT 发送到您的进程(例如,'kill -s INT ');我不确定 KeyboardInterruptException 是否被实现为 SIGINT 处理程序,或者它是否真的只捕获 Ctrl+C 按下,但无论哪种方式,使用信号处理程序都会使您的意图明确(至少,如果您的意图与 OP 相同)。更重要的是,有了信号,您不必将 try-catch 包裹在所有东西上以使其工作,这或多或少是可组合性和通用软件工程的胜利,具体取决于您的应用程序的结构。
    • 为什么要捕获信号而不是捕获异常的示例。假设您运行程序并将输出重定向到日志文件./program.py > output.log。当您按下 Ctrl-C 时,您希望程序通过记录所有数据文件已被刷新并标记为干净以确认它们处于已知的良好状态来优雅地退出。但是 Ctrl-C 将 SIGINT 发送到管道中的所有进程,因此 shell 可能会在 program.py 完成打印最终日志之前关闭 STDOUT(现在是“output.log”)。 Python 会报错,“在文件对象析构函数中关闭失败:sys.excepthook 中的错误:”。
    • 请注意 signal.pause() 在 Windows 上不可用。 docs.python.org/dev/library/signal.html
    • -1 unicorns 用于使用 signal.pause(),表明我将不得不等待这样的阻塞调用,而不是做一些真正的工作。 ;)
    【解决方案2】:

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

    【讨论】:

      【解决方案3】:

      您可以将其视为异常 (KeyboardInterrupt),就像任何其他异常一样。创建一个新文件并使用以下内容从您的 shell 运行它以了解我的意思:

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

      【讨论】:

      • 使用此方案时的注意事项。您还应该在 KeyboardInterrupt 捕获块之前使用此代码:signal.signal(signal.SIGINT, signal.default_int_handler),否则您将失败,因为 KeyboardInterrupt 不会在它应该触发的所有情况下触发!详情here
      • 这对 strg+d 不起作用?即使是之前的信号代码线
      【解决方案4】:

      您可以使用 Python 内置的 signal module 中的函数在 python 中设置信号处理程序。具体来说,signal.signal(signalnum, handler)函数用于为信号signalnum注册handler函数。

      【讨论】:

        【解决方案5】:

        来自 Python 的documentation

        import signal
        import time
        
        def handler(signum, frame):
            print 'Here you go'
        
        signal.signal(signal.SIGINT, handler)
        
        time.sleep(10) # Press Ctrl+c here
        

        【讨论】:

          【解决方案6】:

          作为上下文管理器:

          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
          

          从这里:https://gist.github.com/2907502

          【讨论】:

          • 它也可以在按下 ctrl-C 时抛出一个StopIteration 来打破最里面的循环,对吧?
          • @TheoBelaire 我将创建一个生成器,它接受一个可迭代作为参数并注册/释放信号处理程序,而不是仅仅抛出一个 StopIteration。
          【解决方案7】:

          又一个片段

          main 称为主函数,将exit_gracefully 称为CTRL + c 处理程序

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

          【讨论】:

          • 你应该只使用除了不应该发生的事情。在这种情况下,KeyboardInterrupt 应该会发生。所以这不是一个好的结构。
          • @TristanT 在任何其他语言中是的,但在 Python 中,异常不仅仅针对不应该发生的事情。在 Python 中,使用异常进行流控制(在适当的情况下)实际上被认为是一种很好的风格。
          • 为什么不在except 中只添加exit_gracefully() 而不是传递和添加finally
          • @Break 终于进入,所以exit_gracefully() 也将在main() 完成后被调用
          【解决方案8】:

          我改编了来自@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)和SIGTERMkill <process>

          【讨论】:

          • 你用 SIGTERM 试过了吗? SIGTERM 的原始处理程序似乎不可调用。
          • 我在处理程序中添加了一个打印注释,并获得了 SIGINT 和 SIGTERM 的事件。还显示了向条件 (if h.interupted:) 添加打印。所以我认为它有效。
          【解决方案9】:

          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 这确实是特定于应用程序的,绝对不是本示例的重点。例如,阻塞调用或等待函数不会让 CPU 保持忙碌。此外,这只是如何做事的一个例子。键盘中断通常就足够了,就像其他答案中提到的那样。
          • @ThomasDevoogdt 我喜欢这个解决方案的优雅——我可以完全理解这里发生了什么。您能否评论一下为什么这优于(或劣于)其他解决方案?特别是,我发现 Udi/Cyril N. 的解决方案非常完整,但要复杂得多。
          • 我不喜欢称此解决方案优于其他解决方案。优秀的开发人员会查看不同的答案,并从可用的建议中提炼出自己的特定于应用程序的解决方案。但要提供一个优势,它的使用和实现非常简单。一个相当大的缺点是在处理的线程中需要一个非阻塞循环。
          【解决方案10】:

          就我个人而言,我无法使用 try/except KeyboardInterrupt,因为我使用的是阻塞的标准套接字 (IPC) 模式。所以 SIGINT 被提示了,但只有在套接字上接收到数据后才会出现。

          设置信号处理程序的行为相同。

          另一方面,这仅适用于实际终端。其他启动环境可能不接受 Ctrl+C,或预处理信号。

          另外,Python 中有“Exceptions”和“BaseExceptions”,它们的区别在于解释器需要自己干净地退出,所以有些异常的优先级高于其他异常(Exceptions 派生自 BaseException)

          【讨论】:

            【解决方案11】:

            感谢现有答案,但添加了signal.getsignal()

            import signal
            
            # store default handler of signal.SIGINT
            default_handler = signal.getsignal(signal.SIGINT)
            catch_count = 0
            
            def handler(signum, frame):
                global default_handler, catch_count
                catch_count += 1
                print ('wait:', catch_count)
                if catch_count > 3:
                    # recover handler for signal.SIGINT
                    signal.signal(signal.SIGINT, default_handler)
                    print('expecting KeyboardInterrupt')
            
            signal.signal(signal.SIGINT, handler)
            print('Press Ctrl+c here')
            
            while True:
                pass
            

            【讨论】:

            • signal.getsignal的替代方法是保存signal.signal的返回值,也是一样的。
            【解决方案12】:

            如果您想确保您的清理过程完成,我会使用SIG_IGN 添加到Matt J's answer,以便忽略更多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()
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2011-02-02
              • 1970-01-01
              • 2022-09-23
              • 2021-10-17
              • 1970-01-01
              • 1970-01-01
              • 2011-11-22
              相关资源
              最近更新 更多