Python で KeyboardInterrupt を処理できないのはなぜですか? 質問する

Python で KeyboardInterrupt を処理できないのはなぜですか? 質問する

私は Windows 上で次のような Python 2.6.6 コードを書いています:

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

dostuff()は、入力ストリームから 1 行ずつ読み取り、それに基づいて処理を行いながら、永久にループする関数です。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:句も実行されていません。

編集:コメントに基づいて、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

残念ながら、非同期例外処理は信頼できません (シグナル ハンドラーによって発生した例外、C API 経由の外部コンテキストなど)。コード内で、どのコードが例外をキャッチするかについて調整が行われていれば、非同期例外を適切に処理できる可能性が高まります (非常に重要な関数を除いて、コール スタック内で可能な限り高い位置が適切と思われます)。

呼び出された関数 ( dostuff) またはスタックのさらに下にある関数自体に、考慮していない/考慮できなかった KeyboardInterrupt または BaseException の catch がある可能性があります。

この単純なケースは、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

したがって、この例では、finally ステートメントの実行のコンテキストで KeyboardInterrupt が発生する理由が多少説明されます。

>>> 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() を参照してください。このパターンは、同様の問題に関するこのバグレポートに記載されています: (翻訳: 翻訳者: python)。したがって、シグナルはインタープリターによって不確定な時間に処理されるようです。

sys.stdin.read() の例が "dostuff()" 関数の適切な類似物であるかどうかはまだ明確ではないため、これ以上深く調べる価値はあまりないと思います。(複数のバグが関係している可能性があります)

おすすめ記事