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

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

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". Однако печать не работает нормально - после этого она не печатает новую строку, как предполагалось! "Прерванный" печатается на одной строке... с пробелом перед ним, почему-то...? Во всяком случае, после этого код делает то, что он должен.

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

Ответ 1

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

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

Этот тривиальный случай отлично справился с python 2.6.6 (x64) interactive + Windows 7 (64 бит):

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

EDIT:

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

>>> 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. Он может принимать несколько инструкций байткода до того, как исключение async фактически будет вытолкнуто, а затем поднято в текущем контексте выполнения. (Это поведение, которое я видел, играя с ним в прошлом)

См. 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?revision=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() правильным аналогом вашей функции "dostuff()". (в игре может быть несколько ошибок)

Ответ 2

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

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

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() чтением из stdin?

Ответ 3

Аналогичная проблема и это мой способ:

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()

Ответ 4

Вот догадка о том, что происходит:

  • нажатие Ctrl-C нарушает инструкцию "print" (по какой-то причине... ошибка? Ограничение Win32?)
  • нажатие Ctrl-C также бросает первый KeyboardInterrupt, в dostuff()
  • Обработчик исключений запускается и пытается распечатать "Прервано", но оператор "print" сломан и выдает другой KeyboardInterrupt.
  • Предложение finally выполняется и пытается распечатать "очистка....", но оператор "print" сломан и выдает еще один KeyboardInterrupt.

Ответ 5

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

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

Ответ 6

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

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