Capturez l'interruption du clavier en Python sans essayer, sauf

102

Existe-t-il un moyen en Python de capturer un KeyboardInterruptévénement sans mettre tout le code dans une instruction try- except?

Je veux quitter proprement sans laisser de trace si l'utilisateur appuie sur Ctrl+ C.

Alex
la source

Réponses:

150

Oui, vous pouvez installer un gestionnaire d'interruption à l'aide du signal du module et attendre indéfiniment à l'aide d'un thread .

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()
Johan Kotlinski
la source
10
Notez qu'il y a des problèmes spécifiques à la plate-forme avec le module de signal - ne devrait pas affecter cette affiche, mais "Sous Windows, signal () ne peut être appelé qu'avec SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV ou SIGTERM. Une ValueError sera soulevée dans tous les autres cas. "
bgporter
7
Fonctionne également bien avec les threads. J'espère que vous ne le faites jamais while True: continue, cependant. (Dans ce style, ce while True: passserait plus soigné, de toute façon.) Ce serait très inutile; essayez quelque chose comme while True: time.sleep(60 * 60 * 24)(dormir un jour à la fois est un chiffre entièrement arbitraire).
Chris Morgan
1
Si vous utilisez la suggestion de Chris Morgan d'utiliser time(comme vous devriez), n'oubliez pas de import time:)
Seaux
1
L'appel de sys.exit (0) déclenche une exception SystemExit pour moi. Vous pouvez le faire fonctionner correctement si vous l'utilisez en combinaison avec ceci: stackoverflow.com/a/13723190/353094
leetNightshade
2
Vous pouvez utiliser signal.pause () au lieu de dormir à plusieurs reprises
Croad Langshan
36

Si tout ce que vous voulez est de ne pas afficher le traçage, créez votre code comme ceci:

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


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

(Oui, je sais que cela ne répond pas directement à la question, mais il n'est pas vraiment clair pourquoi avoir besoin d'un bloc try / except est répréhensible - peut-être que cela le rend moins ennuyeux pour l'OP)

bgporter
la source
5
Pour une raison quelconque, cela ne fonctionne pas toujours pour moi. signal.signal( signal.SIGINT, lambda s, f : sys.exit(0))fait toujours.
Hal Canary
Cela ne fonctionne pas toujours avec des éléments tels que pygtk qui utilisent des threads. Parfois, ^ C supprimera simplement le thread actuel au lieu de tout le processus, de sorte que l'exception se propagera uniquement à travers ce thread.
Sudo Bash
Il y a une autre question SO spécifiquement sur Ctrl + C avec pygtk: stackoverflow.com/questions/16410852
...
30

Une alternative à la configuration de votre propre gestionnaire de signal consiste à utiliser un gestionnaire de contexte pour intercepter l'exception et l'ignorer:

>>> 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
... 
>>>

Cela supprime le bloc try- excepttout en conservant une mention explicite de ce qui se passe.

Cela vous permet également d'ignorer l'interruption uniquement dans certaines parties de votre code sans avoir à définir et réinitialiser à nouveau les gestionnaires de signaux à chaque fois.

Bakuriu
la source
1
sympa, cette solution semble un peu plus directe dans l'expression du but plutôt que dans le traitement des signaux.
Seaux
En utilisant la bibliothèque multitraitement, je ne suis pas sûr sur quel objet je dois ajouter ces méthodes .. un indice?
Stéphane
@ Stéphane Que voulez-vous dire? Lorsque vous traitez avec le multitraitement, vous devrez gérer le signal dans les processus parent et enfant, car il pourrait être déclenché dans les deux. Cela dépend vraiment de ce que vous faites et de la manière dont votre logiciel sera utilisé.
Bakuriu
8

Je sais que c'est une vieille question mais je suis venu ici en premier et j'ai découvert le atexitmodule. Je ne connais pas encore ses antécédents multiplateformes ou une liste complète de mises en garde, mais jusqu'à présent, c'est exactement ce que je recherchais en essayant de gérer le post- KeyboardInterruptnettoyage sous Linux. Je voulais juste proposer une autre manière d'aborder le problème.

Je veux faire un nettoyage post-sortie dans le contexte des opérations Fabric, donc emballer tout dans try/ exceptn'était pas non plus une option pour moi. Je pense que cela atexitpeut être un bon choix dans une telle situation, où votre code n'est pas au plus haut niveau du flux de contrôle.

atexit est très capable et lisible hors de la boîte, par exemple:

import atexit

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

atexit.register(goodbye)

Vous pouvez également l'utiliser comme décorateur (à partir de 2.6; cet exemple est tiré de la documentation):

import atexit

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

Si vous souhaitez le rendre spécifique KeyboardInterruptuniquement, la réponse d'une autre personne à cette question est probablement meilleure.

Mais notez que le atexitmodule ne contient que ~ 70 lignes de code et qu'il ne serait pas difficile de créer une version similaire qui traite les exceptions différemment, par exemple en passant les exceptions comme arguments aux fonctions de rappel. (La limitation de atexitcela justifierait une version modifiée: actuellement, je ne peux pas concevoir de moyen pour les fonctions de rappel de sortie de connaître les exceptions; le atexitgestionnaire intercepte l'exception, appelle votre (vos) rappel (s), puis re-déclenche cette exception. Mais vous pouvez le faire différemment.)

Pour plus d'informations, voir:

attrape-dérive
la source
atexit ne fonctionne pas pour KeyboardInterrupt (python 3.7)
TimZaman
Travaillé pour KeyboardInterrupt ici (python 3.7, MacOS). Peut-être une bizarrerie spécifique à la plate-forme?
Niko Nyman le
4

Vous pouvez empêcher l'impression d'une trace de pile pour KeyboardInterrupt, sans try: ... except KeyboardInterrupt: pass(la solution la plus évidente et probablement la «meilleure», mais vous la connaissez déjà et avez demandé autre chose) en remplaçant sys.excepthook. Quelque chose comme

def custom_excepthook(type, value, traceback):
    if type is KeyboardInterrupt:
        return # do nothing
    else:
        sys.__excepthook__(type, value, traceback)
Richard
la source
Je veux une sortie propre sans trace si l'utilisateur appuie sur ctrl-c
Alex
7
Ce n'est pas vrai du tout. L'exception KeyboardInterrupt est créée pendant un gestionnaire d'interruption. Le gestionnaire par défaut de SIGINT déclenche KeyboardInterrupt, donc si vous ne vouliez pas ce comportement, tout ce que vous auriez à faire est de fournir un gestionnaire de signal différent pour SIGINT. Vous avez raison en ce sens que les exceptions ne peuvent être gérées que dans un essai / sauf que dans ce cas, vous pouvez empêcher l'exception d'être déclenchée en premier lieu.
Matt
1
Oui, j'ai appris cela environ trois minutes après la publication, lorsque la réponse de kotlinski est arrivée;)
2

J'ai essayé les solutions suggérées par tout le monde, mais j'ai dû improviser moi-même le code pour le faire fonctionner. Voici mon code improvisé:

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
Rohit Jain
la source
0

Si quelqu'un est à la recherche d'une solution minimale rapide,

import signal

# The code which crashes program on interruption

signal.signal(signal.SIGINT, call_this_function_if_interrupted)

# The code skipped if interrupted
tejasvi88
la source