Як зафіксувати SIGINT в Python?


534

Я працюю над сценарієм python, який запускає кілька процесів і підключення до бази даних. Раз у раз я хочу вбити сценарій із сигналом Ctrl+ C, і я хотів би зробити чистку.

У Perl я зробив би це:

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

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

Як мені зробити аналог цього в Python?

Відповіді:


787

Зареєструйте обробника 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можна знайти тут .  


13
Не могли б ви сказати мені, чому слід використовувати це замість виключення KeyboardInterrupt? Хіба це не інтуїтивніше у використанні?
noio

35
Noio: 2 причини. По-перше, SIGINT може бути надісланий вашому процесу будь-якою кількістю способів (наприклад, 'kill -s INT <pid>'); Я не впевнений, чи KeyboardInterruptException реалізований як обробник SIGINT або якщо він дійсно лише ловить натискання Ctrl + C, але в будь-якому випадку використання обробника сигналів робить ваш намір явним (принаймні, якщо ваш намір збігається з ОП). Що ще важливіше, але за допомогою сигналу вам не доведеться обробляти пробні вигадки навколо всього, щоб змусити їх працювати, що може бути більшою чи меншою складовою придатності та загальної інженерії програмного забезпечення, залежно від структури вашої програми.
Метт Дж

35
Приклад того, чому ви хочете захопити сигнал замість того, щоб зловити виняток. Припустимо , ви запустите програму і перенаправити висновок в файл журналу ./program.py > output.log. Коли ви натискаєте Ctrl-C, ви хочете, щоб ваша програма вийшла граціозно, записуючи її в журнал про те, що всі файли даних були промиті та позначені чистими, щоб підтвердити, що вони залишаються у відомому хорошому стані. Але Ctrl-C посилає SIGINT на всі процеси в конвеєрі, тому оболонка може закрити STDOUT (зараз "output.log") до того, як program.py закінчить друк остаточного журналу. Python поскаржиться, "закрити помилку в деструкторі об'єкта файлу: Помилка в sys.excepthook:".
Noah Spurrier

24
Зауважте, що signal.pause () недоступний у Windows. docs.python.org/dev/library/signal.html
May Oakes

10
-1 єдиноріг для використання signal.pause (), говорить про те, що мені доведеться чекати на такий дзвінок блокування, а не робити якусь справжню роботу. ;)
Нік Т

177

Ви можете трактувати це як виняток (KeyboardInterrupt), як і будь-який інший. Створіть новий файл і запустіть його зі своєї оболонки із наступним вмістом, щоб побачити, що я маю на увазі:

import time, sys

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

22
Увага при використанні цього рішення. Ви також повинні використовувати цей код перед блоком вилову KeyboardInterrupt: signal.signal(signal.SIGINT, signal.default_int_handler)або ви не зможете, оскільки KeyboardInterrupt не спрацьовує в будь-якій ситуації, в якій він повинен стріляти! Деталі тут .
Велда

67

І як контекстний менеджер:

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


Він також може кинути a, StopIterationщоб розірвати найпотаємніший цикл, коли натиснути ctrl-C, правда?
Theo Belaire

@TheoBelaire Замість того, щоб просто кинути StopIteration, я створив би генератор, який приймає ітерабельний параметр і реєструє / звільняє обробник сигналу.
Уді

28

Ви можете обробити CTRL+ C, схопивши KeyboardInterruptвиняток. Ви можете реалізувати будь-який код очищення в обробці винятків.



18

Ще один знімок

Позначається mainяк основна функція та exit_gracefullyяк обробник CTRL+c

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

4
Вам слід користуватися лише за винятком речей, які не повинні відбуватися. У цьому випадку передбачається відбутися KeyboardInterrupt. Тож це не гарне будівництво.
Трістан

15
@TristanT У будь-якій іншій мові так, але в Python винятки не стосуються лише тих речей, які не повинні відбуватися. В Python насправді вважається хорошим стилем використовувати винятки для контролю потоку (де це доречно).
Ян Голдбі

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) та SIGTERM( kill <process>)


5

На відміну від відповіді Метта Дж , я використовую простий предмет. Це дає мені можливість проаналізувати цей обробник на всі потоки, які потребують припинення послідовності.

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 Це дійсно специфічний додаток і, безумовно, не суть цього прикладу. Наприклад, блокування дзвінків або функцій очікування не затримуватимуть процесор. Крім того, це лише приклад того, як все можна зробити. KeyboardInterrupts досить часто, як зазначено в інших відповідях.
Томас Девогдт

4

Ви можете використовувати функції вбудованого сигнального модуля Python для налаштування обробників сигналів у python. Конкретно signal.signal(signalnum, handler)функція використовується для реєстрації handlerфункції для сигналу signalnum.


3

дякую за існуючі відповіді, але додано 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

3

Якщо ви хочете переконатися, що процес очищення закінчується, я додав би відповідь Метта Дж за допомогою 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()

0

Особисто я не міг використати спробувати / крім KeyboardInterrupt, оскільки використовував стандартний режим сокета (IPC), який блокується. Таким чином, SIGINT було встановлено у черзі, але прийшло лише після отримання даних про сокет.

Встановлення обробника сигналу поводиться так само.

З іншого боку, це працює лише для фактичного терміналу. Інші стартові середовища можуть не приймати Ctrl+C або попередньо обробляти сигнал.

Також у Python є "Винятки" та "BaseExceptions", які відрізняються тим сенсом, що інтерпретатору потрібно вийти з чистої для себе, тому деякі винятки мають вищий пріоритет, ніж інші (Винятки походять із BaseException)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.