2013-10-31

StandardError hides KeyboardInterrupt in Python 2.4


私は、Python で次のようなコードを書くことがある。

import datetime

def validate_date_string(s):
    try:
        assert len(s) == 8
        datetime.date(int(s[:4]), int(s[4:6]), int(s[6:]))
        return True
    except StandardError:
        return False

def test():
    assert validate_date_string('20120229') # leap year
    assert not validate_date_string('20130229') # not leap year

StandardError を捕捉するのは、google 先生の教えによる。


  • Never use catch-all except: statements, or catch Exception or StandardError, unless you are re-raising the exception or in the outermost block in your thread (and printing an error message). Python is very tolerant in this regard and except: will really catch everything including misspelled names, sys.exit() calls, Ctrl+C interrupts, unittest failures and all kinds of other exceptions that you simply don't want to catch.

しかしここには、Python バージョンによって例外クラスの階層が異なる、という重要な情報が抜けている。

例えば Python 2.4 以前は KeyboardInterruput が StarndardError のサブクラスになっているため、上記のコードで try ブロック中に Ctrl-C を受けると意図通りに動かなくなる。具体的には、Ctrl-C で終了できず、無条件に False が返ることになる。


Exception
 +-- SystemExit
 +-- StopIteration
 +-- StandardError
 |    +-- KeyboardInterrupt
<snip>


BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- Exception
      +-- GeneratorExit
      +-- StopIteration
      +-- StandardError
     <snip>

Python 2.4 以前でも動くようにするためには、StandardError を処理する前に、KeyboardInterruput を明示的に処理する必要がある。

def validate_date_string(s):
    try:
        assert len(s) == 8
        datetime.date(int(s[:4]), int(s[4:6]), int(s[6:]))
        return True
    except KeyboardInterrupt:
        raise
    except StandardError:
        return False

Python は保守的な言語だと思っていたので、まさかこんな所に罠が有るとは思わなかった。確かに KeyboardInterrupt がエラー扱い(StandardError)なのは不自然であるし、言語の仕様的には正しい修正だとは思うが、正直文句の一つくらいも言いたくなる。

これでまた Python が一つ嫌いになった:-p。