¿Cómo capturo SIGINT en Python?

536

Estoy trabajando en un script de Python que inicia varios procesos y conexiones de bases de datos. De vez en cuando quiero matar el script con una señal Ctrl+ C, y me gustaría hacer una limpieza.

En Perl haría esto:

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

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

¿Cómo hago el análogo de esto en Python?

James Thompson
fuente

Respuestas:

787

Registre su controlador con signal.signaleste:

#!/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()

Código adaptado desde aquí .

signalPuede encontrar más documentación sobre aquí .  

Matt J
fuente
13
¿Podría decirme por qué usar esto en lugar de una excepción KeyboardInterrupt? ¿No es más intuitivo de usar?
noio
35
Noio: 2 razones. Primero, SIGINT puede enviarse a su proceso de varias maneras (por ejemplo, 'kill -s INT <pid>'); No estoy seguro de si KeyboardInterruptException se implementa como un controlador SIGINT o si realmente solo presiona Ctrl + C, pero de cualquier manera, el uso de un controlador de señal hace que su intención sea explícita (al menos, si su intención es la misma que la de OP). Sin embargo, lo más importante es que, con una señal, no tiene que ajustar las pruebas de detección de todo para que funcionen, lo que puede ser más o menos una posibilidad de componer y la ingeniería de software general depende de la estructura de su aplicación.
Matt J
35
Ejemplo de por qué desea atrapar la señal en lugar de atrapar la excepción. Digamos que ejecuta su programa y redirigir la salida a un archivo de registro, ./program.py > output.log. Cuando presiona Ctrl-C , desea que su programa salga correctamente haciendo que registre que todos los archivos de datos se han vaciado y marcado como limpio para confirmar que se han dejado en un buen estado conocido. Pero Ctrl-C envía SIGINT a todos los procesos en una tubería, por lo que el shell puede cerrar STDOUT (ahora "output.log") antes de que program.py termine de imprimir el registro final. Python se quejará, "error de cierre en el destructor de objetos de archivo: Error en sys.excepthook:".
Noah Spurrier
24
Tenga en cuenta que signal.pause () no está disponible en Windows. docs.python.org/dev/library/signal.html
Mayo Oakes
10
-1 unicornios para usar signal.pause (), sugiere que tendría que esperar a una llamada de bloqueo en lugar de hacer un trabajo real. ;)
Nick T
177

Puede tratarlo como una excepción (KeyboardInterrupt), como cualquier otro. Cree un nuevo archivo y ejecútelo desde su shell con los siguientes contenidos para ver a qué me refiero:

import time, sys

x = 1
while True:
    try:
        print x
        time.sleep(.3)
        x += 1
    except KeyboardInterrupt:
        print "Bye"
        sys.exit()
rledley
fuente
22
Atención al usar esta solución. También debe usar este código antes del bloque de captura KeyboardInterrupt: ¡ signal.signal(signal.SIGINT, signal.default_int_handler)o va a fallar, porque KeyboardInterrupt no se dispara en todas las situaciones en las que debería dispararse! Los detalles están aquí .
Velda
67

Y como gestor de contexto:

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

Usar:

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

Manejadores anidados:

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

Desde aquí: https://gist.github.com/2907502

Udi
fuente
También podría arrojar un StopIterationpara romper el bucle más interno cuando se presiona un ctrl-C, ¿verdad?
Theo Belaire
@TheoBelaire En lugar de simplemente lanzar una StopIteration, crearía un generador que acepte un iterable como parámetro y registre / libere el controlador de señal.
Udi
28

Puede manejar CTRL+ Ccapturando la KeyboardInterruptexcepción. Puede implementar cualquier código de limpieza en el controlador de excepciones.

Jay Conrod
fuente
21

De la documentación de 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
sunqiang
fuente
19

Otro fragmento

Referido maincomo la función principal y exit_gracefullycomo el manejador CTRL+c

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass
    finally:
        exit_gracefully()
Jossef Harush
fuente
44
Solo debe usar, excepto para cosas que no se supone que sucedan. En este caso, se supone que ocurre KeyboardInterrupt. Entonces esta no es una buena construcción.
Tristan
16
@TristanT En cualquier otro idioma sí, pero en Python las excepciones no son solo para cosas que no se supone que sucedan. En realidad, se considera un buen estilo en Python usar excepciones para el control de flujo (cuando corresponda).
Ian Goldby el
8

Adapte el código de @udi para admitir múltiples señales (nada lujoso):

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

Este código admite la llamada de interrupción del teclado ( SIGINT) y el SIGTERM( kill <process>)

Cyril N.
fuente
5

En contraste con Matt J su respuesta, yo uso un objeto simple. Esto me da la posibilidad de analizar este controlador en todos los subprocesos que deben detenerse.

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)

En otra parte

while True:
    # task
    if handler.SIGINT:
        break
Thomas Devoogdt
fuente
Debe usar un evento o en time.sleep()lugar de hacer un ciclo ocupado en una variable.
OlivierM
@OlivierM Esto es realmente específico de la aplicación y definitivamente no es el punto de este ejemplo. Por ejemplo, bloquear llamadas o esperar funciones no mantendrá ocupada la CPU. Además, este es solo un ejemplo de cómo se pueden hacer las cosas. Las interrupciones del teclado suelen ser suficientes, como se menciona en otras respuestas.
Thomas Devoogdt
4

Puede usar las funciones en el módulo de señal incorporado de Python para configurar manejadores de señal en python. Específicamente, la signal.signal(signalnum, handler)función se utiliza para registrar la handlerfunción para la señal signalnum.

Brandon E Taylor
fuente
3

gracias por las respuestas existentes, pero agregó 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
gsw945
fuente
3

Si desea asegurarse de que finalice su proceso de limpieza, agregaría a la respuesta de Matt J utilizando un SIG_IGN para que SIGINTse ignoren aún más, lo que evitará que se interrumpa su limpieza.

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()
Josh Correia
fuente
0

Personalmente, no pude usar try / except KeyboardInterrupt porque estaba usando el modo de socket estándar (IPC) que está bloqueando. Por lo tanto, el SIGINT se activó, pero llegó solo después de recibir datos en el zócalo.

Establecer un controlador de señal se comporta igual.

Por otro lado, esto solo funciona para un terminal real. Otros entornos de inicio podrían no aceptar Ctrl+ Co manejar previamente la señal.

Además, hay "Excepciones" y "BaseExceptions" en Python, que difieren en el sentido de que el intérprete debe salir de forma limpia, por lo que algunas excepciones tienen mayor prioridad que otras (las excepciones se derivan de BaseException)

Hatebit
fuente