Przechwytuj przerwanie klawiatury w Pythonie bez try-except

102

Czy w Pythonie jest sposób na przechwycenie KeyboardInterruptzdarzenia bez umieszczania całego kodu w instrukcji try- except?

Chcę czysto wyjść bez śladu, jeśli użytkownik naciśnie Ctrl+ C.

Alex
źródło

Odpowiedzi:

150

Tak, możesz zainstalować program obsługi przerwań za pomocą sygnału modułu i czekać wiecznie używając wątków .

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
źródło
10
Zauważ, że istnieją pewne problemy specyficzne dla platformy z modułem sygnału - nie powinno to mieć wpływu na ten plakat, ale „W systemie Windows funkcja signal () może być wywołana tylko za pomocą SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV lub SIGTERM. A ValueError zostanie podniesiony w każdym innym przypadku. ”
bgporter
7
Działa również dobrze z wątkami. Mam jednak nadzieję, że nigdy tego nie zrobisz while True: continue. (W tym stylu i tak while True: passbyłoby schludniej.) Byłoby to bardzo marnotrawne; spróbuj czegoś w rodzaju while True: time.sleep(60 * 60 * 24)(spanie przez jeden dzień jest całkowicie arbitralną postacią).
Chris Morgan,
1
Jeśli korzystasz z sugestii Chrisa Morgana, aby używać time(tak, jak powinieneś), nie zapomnij o import time:)
Seaux
1
Wywołanie sys.exit (0) wyzwala dla mnie wyjątek SystemExit. Możesz sprawić, by działało dobrze, jeśli użyjesz go w połączeniu z tym: stackoverflow.com/a/13723190/353094
leetNightshade
2
Możesz użyć signal.pause () zamiast spać wielokrotnie
Croad Langshan.
36

Jeśli chcesz tylko nie pokazywać śledzenia, zrób swój kod w ten sposób:

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


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

(Tak, wiem, że to nie jest bezpośrednią odpowiedzią na pytanie, ale nie jest do końca jasne, dlaczego potrzeba blokowania prób / wyjątków jest niepożądana - może to sprawia, że ​​jest mniej irytująca dla OP)

bgporter
źródło
5
Z jakiegoś powodu to nie zawsze działa dla mnie. signal.signal( signal.SIGINT, lambda s, f : sys.exit(0))zawsze to robi.
Hal Canary
Nie zawsze działa to w przypadku rzeczy takich jak pygtk, które używają wątków. Czasami ^ C po prostu zabija bieżący wątek zamiast całego procesu, więc wyjątek będzie propagowany tylko przez ten wątek.
Sudo Bash,
Jest jeszcze jedno SO pytanie dotyczące Ctrl + C z pygtk: stackoverflow.com/questions/16410852/ ...
bgporter
30

Alternatywą dla ustawienia własnego modułu obsługi sygnału jest użycie menedżera kontekstu do przechwycenia wyjątku i zignorowania go:

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

To usuwa blok try- except, zachowując jednocześnie wyraźną wzmiankę o tym, co się dzieje.

Pozwala to również zignorować przerwanie tylko w niektórych częściach kodu bez konieczności ustawiania i resetowania za każdym razem programów obsługi sygnału.

Bakuriu
źródło
1
fajnie, to rozwiązanie wydaje się nieco bardziej bezpośrednie w wyrażaniu celu niż zajmowaniu się sygnałami.
Seaux
Korzystając z biblioteki wieloprocesorowej, nie jestem pewien, w którym obiekcie powinienem dodać te metody .. jakaś wskazówka?
Stéphane
@ Stéphane Co masz na myśli? W przypadku przetwarzania wieloprocesowego będziesz musiał radzić sobie z sygnałem zarówno w procesie nadrzędnym, jak i podrzędnym, ponieważ może on zostać wyzwolony w obu. To naprawdę zależy od tego, co robisz i jak będzie używane oprogramowanie.
Bakuriu
8

Wiem, że to stare pytanie, ale najpierw przyszedłem tutaj, a potem odkryłem atexitmoduł. Nie wiem jeszcze o jego osiągnięciach na wielu platformach ani o pełnej liście zastrzeżeń, ale jak dotąd jest to dokładnie to, czego szukałem, próbując obsłużyć po KeyboardInterruptczyszczeniu w systemie Linux. Chciałem tylko rzucić okiem na inny sposób podejścia do problemu.

Chcę przeprowadzić porządki po wyjściu z programu w kontekście operacji Fabric, więc zawijanie wszystkiego w try/ exceptnie było dla mnie opcją. Wydaje mi się, że atexitmoże być dobrym rozwiązaniem w takiej sytuacji, w której Twój kod nie znajduje się na najwyższym poziomie przepływu kontroli.

atexit jest bardzo wydajny i czytelny po wyjęciu z pudełka, na przykład:

import atexit

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

atexit.register(goodbye)

Możesz również użyć go jako dekoratora (od 2.6; ten przykład pochodzi z dokumentacji):

import atexit

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

Jeśli chcesz, aby było to specyficzne KeyboardInterrupttylko dla, odpowiedź innej osoby na to pytanie jest prawdopodobnie lepsza.

Należy jednak pamiętać, że atexitmoduł zawiera tylko ~ 70 linii kodu i nie byłoby trudno stworzyć podobną wersję, która traktuje wyjątki inaczej, na przykład przekazując wyjątki jako argumenty do funkcji wywołania zwrotnego. (Ograniczenie atexitto gwarantowałoby zmodyfikowaną wersję: obecnie nie mogę sobie wyobrazić sposobu, w jaki funkcje wywołania zwrotnego wyjścia miałyby wiedzieć o wyjątkach; program atexitobsługi przechwytuje wyjątek, wywołuje wywołanie zwrotne, a następnie ponownie wywołuje ten wyjątek. Ale możesz zrobić to inaczej).

Więcej informacji:

driftcatcher
źródło
atexit nie działa dla KeyboardInterrupt (python 3.7)
TimZaman
Pracował dla KeyboardInterrupt tutaj (python 3.7, MacOS). Może dziwactwo specyficzne dla platformy?
Niko Nyman
Potwierdza, że atexitdziała zarówno dla MacOS, jak i Ubuntu 18.04 dla Pythona 3.7 i 3.8
Ainz Titor
4

Możesz zapobiec drukowaniu śladu stosu KeyboardInterruptbez try: ... except KeyboardInterrupt: pass(najbardziej oczywistego i prawdopodobnie „najlepszego” rozwiązania, ale już go znasz i prosiłeś o coś innego), zastępując sys.excepthook. Coś jak

def custom_excepthook(type, value, traceback):
    if type is KeyboardInterrupt:
        return # do nothing
    else:
        sys.__excepthook__(type, value, traceback)
Richard
źródło
Chcę wyczyścić wyjście bez śladu, jeśli użytkownik naciśnie ctrl-c
Alex
7
To wcale nie jest prawdą. Wyjątek KeyboardInterrupt jest tworzony podczas obsługi przerwań. Domyślna procedura obsługi dla SIGINT podnosi KeyboardInterrupt, więc jeśli nie chcesz tego zachowania, wszystko, co musisz zrobić, to zapewnić inną obsługę sygnału dla SIGINT. Masz rację co do tego, że wyjątki można obsłużyć tylko w trybie try /, z wyjątkiem tego, że w tym przypadku możesz przede wszystkim zapobiec zgłaszaniu wyjątku.
Matt
1
Tak, dowiedziałem się, że około trzy minuty po wysłaniu, gdy nadeszła odpowiedź Kotlińskiego;)
2

Wypróbowałem sugerowane rozwiązania przez wszystkich, ale musiałem sam improwizować kod, aby faktycznie zadziałał. Oto mój improwizowany kod:

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
źródło
0

Jeśli ktoś szuka szybkiego, minimalnego rozwiązania,

import signal

# The code which crashes program on interruption

signal.signal(signal.SIGINT, call_this_function_if_interrupted)

# The code skipped if interrupted
tejasvi88
źródło