Почему я не могу обработать KeyboardInterrupt в python?

Я пишу код Python 2.6.6 для Windows, который выглядит так:

try:
    dostuff()
except KeyboardInterrupt:
    print "Interrupted!"
except:
    print "Some other exception?"
finally:
    print "cleaning up...."
    print "done."

dostuff() — это функция, которая вечно зацикливается, считывая строку за раз из входного потока и воздействуя на нее. Я хочу иметь возможность остановить его и очистить, когда я нажму ctrl-c.

Вместо этого происходит то, что код под except KeyboardInterrupt: вообще не работает. Единственное, что печатается, это "очистка...", а затем печатается трассировка, которая выглядит так:

Traceback (most recent call last):
  File "filename.py", line 119, in <module>
    print 'cleaning up...'
KeyboardInterrupt

Таким образом, код обработки исключений НЕ выполняется, а трассировка утверждает, что KeyboardInterrupt произошел во время предложения finally, что не имеет смысла, поскольку нажатие Ctrl-C привело к тому, что эта часть запустилась в первом место! Даже общее предложение except: не работает.

EDIT: Основываясь на комментариях, я заменил содержимое блока try: на sys.stdin.read(). Проблема по-прежнему возникает именно так, как описано, когда выполняется первая строка блока finally:, а затем печатается та же самая трассировка.

РЕДАКТИРОВАНИЕ №2: если я добавляю что-либо после чтения, обработчик работает. Итак, это не удается:

try:
    sys.stdin.read()
except KeyboardInterrupt:
    ...

Но это работает:

try:
    sys.stdin.read()
    print "Done reading."
except KeyboardInterrupt:
    ...

Вот что напечатано:

Done reading. Interrupted!
cleaning up...
done.

Итак, почему-то "Прочитано". печатается строка, даже если исключение произошло в предыдущей строке. На самом деле это не проблема - очевидно, я должен иметь возможность обрабатывать исключение в любом месте внутри блока "try". Однако печать не работает нормально - после этого не печатается новая строка, как предполагалось! «Прервано» напечатано на той же строке... с пробелом перед ним, по какой-то причине...? Во всяком случае, после этого код делает то, что должен.

Мне кажется, что это баг обработки прерывания во время заблокированного системного вызова.


person Josh    schedule 05.01.2011    source источник
comment
Покажите код вашего dostuff(), потому что этот код должен работать (и он работает)   -  person user225312    schedule 05.01.2011
comment
Он работает, как и ожидалось, с Python 2.5.1.   -  person khachik    schedule 05.01.2011
comment
воспроизведено с помощью Python 2.7, заменив dostuff() на sys.stdin.read()   -  person balpha    schedule 05.01.2011
comment
Отлично работает с 2.6.6, заменяя dostuff() на sys.stdin.read()   -  person user225312    schedule 05.01.2011
comment
@balpha, когда вы говорите «воспроизведено», вы имеете в виду, что тоже столкнулись с моей проблемой?   -  person Josh    schedule 05.01.2011
comment
Да; Я запустил именно ваш код (за исключением упомянутой замены dostuff()) в Python 2.7 в 64-разрядной версии Windows 7 и вижу точно такие же результаты, которые вы описываете.   -  person balpha    schedule 05.01.2011
comment
С другой стороны, в Fedora 14 (также Python 2.7) он работает так, как ожидалось.   -  person balpha    schedule 05.01.2011
comment
Я также могу воспроизвести это с Python 2.6.6, 2.7.1 и 3.1.3 на Win7 x64. Вставка sys.stdout.flush() после print "Interrupted!" тоже ничего не изменила.   -  person Tim Pietzcker    schedule 05.01.2011
comment
Я также запускаю python на 64-разрядной версии Windows 7. Однако я использую 32-битный python.   -  person Josh    schedule 05.01.2011
comment
воспроизведено на 32-битном интерпретаторе python 2.6.5 на 64-битной ОС Win7. заменил do_stuff() на sys.stdin.read(). первый вывод после очистки ctl-c...   -  person Corey Goldberg    schedule 05.01.2011
comment
только что заметил, что если вы замените do_stuff() на while True: pass или time.sleep(10), ctl-c перехватывается/обрабатывается правильно.   -  person Corey Goldberg    schedule 05.01.2011
comment
@balpha: что произойдет, если вы замените dostuff() на sys.stdin.readline()?   -  person ChristopheD    schedule 05.01.2011
comment
@ChristopheD: то же самое   -  person balpha    schedule 05.01.2011
comment
если вы замените do_stuff() на sys.stdin.read(); time.sleep(.001), он справляется с этим правильно. если вы пропустите time.sleep(), он не поймает ctl-c. странный.   -  person Corey Goldberg    schedule 05.01.2011
comment
@Corey - в моем обновленном ответе есть теория о том, почему он зависит от времени / num_bytecode_instructions_executed.   -  person Jeremy Brown    schedule 05.01.2011


Ответы (6)


К сожалению, асинхронная обработка исключений ненадежна (исключения, вызванные обработчиками сигналов, вне контекста через C API и т. д.). Вы можете увеличить свои шансы на правильную обработку асинхронного исключения, если в коде есть некоторая координация того, какой фрагмент кода отвечает за их перехват (максимально возможный уровень в стеке вызовов кажется подходящим, за исключением очень важных функций).

Вызываемая функция (dostuff) или функции, расположенные дальше по стеку, могут сами иметь перехват для KeyboardInterrupt или BaseException, который вы не учли/не смогли учесть.

Этот тривиальный случай отлично работал с Python 2.6.6 (x64) интерактивный + Windows 7 (64-битная):

>>> import time
>>> def foo():
...     try:
...             time.sleep(100)
...     except KeyboardInterrupt:
...             print "INTERRUPTED!"
...
>>> foo()
INTERRUPTED!  #after pressing ctrl+c

ИЗМЕНИТЬ:

После дальнейшего исследования я попробовал то, что, как мне кажется, является примером, который другие использовали для воспроизведения проблемы. Мне было лень, поэтому я пропустил "наконец"

>>> def foo():
...     try:
...             sys.stdin.read()
...     except KeyboardInterrupt:
...             print "BLAH"
...
>>> foo()

Это возвращается сразу после нажатия CTRL+C. Интересное произошло, когда я тут же снова попытался вызвать foo:

>>> foo()

Traceback (most recent call last):
  File "c:\Python26\lib\encodings\cp437.py", line 14, in decode
    def decode(self,input,errors='strict'):
KeyboardInterrupt

Исключение возникло сразу, без нажатия CTRL+C.

Казалось бы, в этом есть смысл — похоже, мы имеем дело с нюансами обработки асинхронных исключений в Python. Может потребоваться несколько инструкций байт-кода, прежде чем асинхронное исключение будет фактически извлечено, а затем вызвано в текущем контексте выполнения. (Это поведение, которое я видел, когда играл с ним в прошлом)

См. C API: http://docs.python.org/c-api/init.html#PyThreadState_SetAsyncExc

Так что это в некоторой степени объясняет, почему KeyboardInterrupt вызывается в контексте выполнения оператора finally в этом примере:

>>> def foo():
...     try:
...             sys.stdin.read()
...     except KeyboardInterrupt:
...             print "interrupt"
...     finally:
...             print "FINALLY"
...
>>> foo()
FINALLY
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in foo
KeyboardInterrupt

Может быть какое-то сумасшедшее смешение пользовательских обработчиков сигналов, смешанных со стандартным обработчиком KeyboardInterrupt/CTRL+C интерпретатора, что приводит к такому поведению. Например, вызов read() видит сигнал и отменяет его, но повторно инициирует сигнал после отмены регистрации обработчика. Я бы не знал наверняка, не проверив кодовую базу интерпретатора.

Вот почему я обычно избегаю использования асинхронных исключений....

ИЗМЕНИТЬ 2

Я думаю, что есть хороший повод для отчета об ошибке.

Опять же, больше теорий... (только на основе чтения кода). См. источник файлового объекта: http://svn.python.org/view/python/branches/release26-maint/Objects/fileobject.c?ревизия=81277&view=markup

file_read вызывает Py_UniversalNewlineFread(). fread может вернуться с ошибкой errno = EINTR (она выполняет собственную обработку сигналов). В этом случае Py_UniversalNewlineFread() спасает, но не выполняет никакой проверки сигналов с помощью PyErr_CheckSignals(), чтобы обработчики могли вызываться синхронно. file_read очищает ошибку файла, но также не вызывает PyErr_CheckSignals().

См. getline() и getline_via_fgets() для примеров того, как они используются. Шаблон задокументирован в этом отчете об ошибке для аналогичной проблемы: ( http://bugs.python.org/issue1195). Таким образом, кажется, что интерпретатор обрабатывает сигнал в неопределенное время.

Я предполагаю, что нет смысла копать глубже, так как до сих пор не ясно, является ли пример sys.stdin.read() правильным аналогом вашей функции "dstuff()". (в игре может быть несколько ошибок)

person Jeremy Brown    schedule 05.01.2011
comment
Но другие (включая меня - 32-битная Windows 7 с Python3 здесь) воспроизвели его с заменами dostuff, которые не перехватывают KeyboardInterrupt. - person ; 05.01.2011
comment
@delnan - ответ расширен. надеюсь, что это поможет в формулировании некоторых теорий! :) - person Jeremy Brown; 05.01.2011
comment
Я бы не назвал это нюансом в асинхронных исключениях, а откровенной ошибкой. Если исключение возникает немного позже, чем когда это технически происходит, это нормально... но если исключение возникает ВНЕ блока try, из которого возникло исключение, это другая история. Здесь явно нарушено несколько хорошо задокументированных поведений, наиболее важно то, что блок finally не запускает гарантированный очищающий код; а также то, что обработчик исключений вообще не запускается, когда в блоке try однозначно возникает хотя бы одно исключение. - person Josh; 05.01.2011
comment
@ Джош: я полностью согласен. Хотя ответ Джереми Брауна - хорошая статья, я бы не назвал его окончательным ответом по этому вопросу (лично я не могу воспроизвести его в своих сборках Python). Конечно, это поведение (ошибка или нет) должно быть где-то задокументировано? - person ChristopheD; 05.01.2011

sys.stdin.read() — это системный вызов, поэтому поведение будет отличаться для каждой системы. Для Windows 7 я думаю, что происходит то, что ввод буферизуется, и поэтому вы получаете, где sys.stdin.read() возвращает все до Ctrl-C, и как только вы снова получите доступ к sys.stdin, он отправит «Ctrl- С".

попробуйте следующее,

def foo():
    try:
        print sys.stdin.read()
        print sys.stdin.closed
    except KeyboardInterrupt:
        print "Interrupted!"

Это говорит о том, что происходит буферизация stdin, из-за которой другой вызов stdin распознает ввод с клавиатуры.

def foo():
    try:
        x=0
        while 1:
            x += 1
        print x
    except KeyboardInterrupt:
        print "Interrupted!"

не кажется, что проблема.

dostuff() читает со стандартного ввода?

person milkypostman    schedule 05.01.2011
comment
Я думаю, вы что-то поняли, за исключением того, что если вы замените sys.stdin.closed любой другой командой, включая печать или сон, это также заставит ее работать. Я не думаю, что доступ к стандартному вводу во второй раз необходим... Я думаю, что, возможно, нажатие ctrl-c приводит к возврату системного вызова, а затем ТАКЖЕ отправляет прерывание в программное обеспечение, но python не обрабатывает прерывание до тех пор, пока не будет следующая строка кода... к тому времени она уже вне блока try! - person Josh; 05.01.2011
comment
Верно, но я не уверен, что вы делаете в своем dostuff(), чтобы это не произошло. - person milkypostman; 05.01.2011

Имея аналогичную проблему, и это мой обходной путь:

try:
    some_blocking_io_here() # CTRL-C to interrupt
except:
    try:
        print() # any i/o will get the second KeyboardInterrupt here?
    except:
        real_handler_here()
person coqem2    schedule 15.07.2014

Вот предположение о том, что происходит:

  • нажатие Ctrl-C прерывает оператор "print" (по какой-то причине... ошибка? Ограничение Win32?)
  • нажатие Ctrl-C также вызывает первый KeyboardInterrupt в dostuff()
  • Обработчик исключений запускается и пытается напечатать «Прервано», но оператор «печать» не работает и вызывает еще один прерывание клавиатуры.
  • Предложение finally запускается и пытается напечатать «очистка…», но оператор «print» не работает и вызывает еще один прерывание клавиатуры.
person user9876    schedule 05.01.2011
comment
Вам удалось воспроизвести проблему? Винда я полагаю? - person user225312; 05.01.2011
comment
Исключение никак не может сломать print (или любую другую функцию в этом отношении). Кроме того, это приведет к n-кратному стеку с обработкой вышеуказанного исключения, между которым возникло другое исключение. - person ; 05.01.2011

Это работает для меня:

import sys

if __name__ == "__main__":
    try:
        sys.stdin.read()
        print "Here"
    except KeyboardInterrupt:
        print "Worked"
    except:
        print "Something else"
    finally:
        print "Finally"

Попробуйте поместить строку вне функции dostuff() или переместить условие цикла за пределы функции. Например:

try:
    while True:
        dostuff()
except KeyboardInterrupt:
    print "Interrupted!"
except:
    print "Some other exception?"
finally:
    print "cleaning up...."
    print "done."
person Jake    schedule 05.01.2011

def foo():
    try:
        x=0
        while 1:
            x+=1
            print (x)
    except KeyboardInterrupt:
       print ("interrupted!!")
foo()

Это прекрасно работает.

person Heinz Doepkemeier    schedule 29.08.2015