Не удается дважды запустить KeyboardInterrupt в командной строке?

Сегодня мне нужно было проверить, как работает мой script в командной строке Windows [1] когда я заметил что-то странное. Я работал над чем-то подобным, но этого достаточно, чтобы продемонстрировать проблему. Вот код.

def bing():
    try:
        raw_input()
    except KeyboardInterrupt:
        print 'This is what actually happened here!'

try:                     # pardon me for those weird strings
    bing()               # as it consistent with everything in the chat room (see below)
    print 'Yoo hoo...'
except KeyboardInterrupt:
    print 'Nothing happens here too!'

Вот ситуация. Когда выполняется script, он ждет ввода, и пользователь должен нажать Ctrl + C, чтобы поднять KeyboardInterrupt, который (должен) был бы улавливан блоком except в пределах bing(). Таким образом, это должен быть фактический результат. И это происходит, когда я запускаю его на своем терминале Ubuntu и IDLE (как на Windows, так и на Ubuntu).

This is what actually happened here!
Yoo hoo...

Но это не так, как ожидалось, в командной строке Windows. Я скорее получаю странный результат.

This is what actually happened here! Nothing happens here too!

Похоже, что один KeyboardInterrupt распространяется по всей программе и, наконец, завершает его.

Я пробовал все, что мог. Во-первых, я использовал signal.signal для обработки SIGINT (который не работал), а затем я использовал функцию обработки, чтобы поднять Exception, который я позже поймал (и это тоже не сработало), и тогда все усложнилось, чем раньше. Итак, я приземлился на старый добрый try... catch. Затем я пошел в комнату для питонистов.

@poke предложил, что при нажатии Ctrl + C повышается EOFError. Затем @ZeroPiraeus сказал, что EOFError возникает при нажатии Ctrl + Z и Enter.

Это было полезно, что привело к обсуждению после нескольких минут прокрутки. Вскоре все стало хаосом! Некоторые результаты были хорошими, некоторые были неожиданными, и несколько пошли haywire!

Weirdo!

Вывод состоял в том, чтобы прекратить использование Windows и попросить моих друзей использовать терминал (я согласен). Однако я мог бы сделать обходной путь, поймав EOFError вместе с KeyboardInterrupt. Хотя лениво нажимать Ctrl + Z и Enter каждый раз, это не большая проблема для меня. Но для меня это одержимость.

В ходе дальнейших исследований я также заметил, что там нет KeyboardInterrupt, поднятых на CMD, когда я нажимаю Ctrl + C.

Whaa???

Там ничего нет. Итак, что здесь происходит? Почему распространяется KeyboardInterrupt? Есть ли способ (вообще) сделать вывод совместимым с терминалом?


[1]: Я всегда работал на терминале, но сегодня мне нужно было убедиться, что мой script работает на всех платформах (особенно потому, что большинство моих друзей не являются кодовыми и просто привязаны к Windows).

Ответ 1

Вопрос user2357112 связан, объясняет это каким-то образом: Почему я не могу обработать KeyboardInterrupt в python?.

Прерывание клавиатуры повышается асинхронно, поэтому он не сразу завершает работу приложения. Вместо этого Ctrl+C обрабатывается в каком-то контуре событий, который занимает некоторое время, чтобы добраться туда. Это, к сожалению, означает, что вы не можете надежно поймать KeyboardInterrupt в этом случае. Но мы можем сделать кое-что, чтобы добраться туда.

Как я объяснил вчера, исключение, которое останавливает вызов raw_input, не является KeyboardInterrupt, а EOFError. Вы можете легко проверить это, изменив функцию bing следующим образом:

def bing():
    try:
        raw_input()
    except Exception as e:
        print(type(e))

Вы увидите, что напечатанный тип исключения EOFError, а не KeyboardInterrupt. Вы также увидите, что print даже не прошел полностью: нет новой строки. Похоже, что выход был прерван прерыванием, которое появилось сразу после того, как оператор print написал тип исключения для stdout. Вы также можете увидеть это, когда вы добавляете немного больше материала в печать:

def bing():
    try:
        raw_input()
    except EOFError as e:
        print 'Exception raised:', 'EOF Error'

Обратите внимание, что Im использует два отдельных аргумента для оператора печати. Когда мы это выполним, мы можем увидеть текст "Исключено", но "Ошибка EOF" не появится. Вместо этого будет вызываться except от внешнего вызова и прерывается прерывание клавиатуры.

Тем не менее, в Python 3 ситуация немного вышла из-под контроля. Возьмите этот код:

def bing():
    try:
        input()
    except Exception as e:
        print('Exception raised:', type(e))

try:
    bing()
    print('After bing')
except KeyboardInterrupt:
    print('Final KeyboardInterrupt')

Это в значительной степени то, что мы делали раньше, просто исправлены для синтаксиса Python 3. Если я запустил это, я получаю следующий вывод:

Exception raised: <class 'EOFError'>
After bing
Final KeyboardInterrupt

Итак, мы можем снова увидеть, что EOFError правильно пойман, но по какой-то причине Python 3 продолжает выполнение намного дольше, чем Python 2 здесь, так как выполняется печать после bing(). Хуже того, в некоторых исполнениях с cmd.exe я получаю результат, что прерывание клавиатуры не было обнаружено вообще (так что, по-видимому, прерывание получило обработку после завершения программы).

Итак, что мы можем сделать с этим, если хотим убедиться, что мы получим прерывание клавиатуры? Мы точно знаем, что прерывание подсказки input() (или raw_input()) всегда вызывает EOFError: То, что мы постоянно видели. Итак, что мы можем сделать, это просто поймать это, а затем убедиться, что мы получим прерывание клавиатуры.

Один из способов сделать это - просто поднять KeyboardInterrupt из обработчика исключений для EOFError. Но это не только чувствует себя немного грязным, но и не гарантирует, что прерывание на самом деле является тем, что в первую очередь прекратило вводное приглашение (кто знает, что еще может поднять EOFError?). Таким образом, мы должны иметь уже существующий сигнал прерывания, генерирующий исключение.

То, как мы это делаем, довольно просто: мы ждем. До сих пор наша проблема заключалась в том, что исполнение продолжалось, потому что исключение не достигало достаточно быстро. Итак, что, если мы немного подождем, чтобы исключение получилось, прежде чем мы продолжим с другими вещами?

import time
def bing():
    try:
        input() # or raw_input() for Python 2
    except EOFError:
        time.sleep(1)

try:
    bing()
    print('After bing')
except KeyboardInterrupt:
    print('Final KeyboardInterrupt')

Теперь мы просто поймаем EOFError и немного подождем, чтобы асинхронные процессы в задней части уладились и решили, нарушить выполнение или нет. Это позволяет мне поймать KeyboardInterrupt во внешнем try/catch и не будет печатать ничего, кроме того, что я делаю в обработчике исключений.

Вы можете опасаться, что одна секунда - долго ждать, но в наших случаях, когда мы прерываем выполнение, эта секунда никогда не длится долго. Всего за несколько миллисекунд после time.sleep прерывание поймано и было в нашем обработчике исключений. Таким образом, одна секунда - просто отказоустойчивость, которая будет ждать достаточно долго, чтобы исключение, безусловно, прибыло вовремя. И в худшем случае, когда на самом деле нет прерывания, а просто "нормального" EOFError? Тогда программа, которая ранее блокировалась бесконечно для ввода пользователем, займет секунду дольше, чтобы продолжить; это не должно быть проблемой всегда (не говоря уже о том, что EOFError, вероятно, очень редко).

Итак, у нас есть наше решение: просто поймайте EOFError и подождите немного. По крайней мере, я надеюсь, что это решение, которое работает на других машинах, чем моя собственная ^ _ ^ "После прошлой ночи я не слишком уверен в этом, но по крайней мере я получил последовательный опыт над всеми терминалами и разными версиями Python.