あまり知られていないけれど、便利なPythonの構文3つを紹介。どれもちょっとしたことだが、汎用的で使える場面が多い言語構文。他の言語には同等の機能がないことが多いから知られていないのだと思う。
ちなみにPython3のお話、Python2にあるのかは知らん。
for-else、while-else 文
for
の後ろにelse
句をつけると、ループの中でbreak
しなかった場合のみ実行してくれる。while
も同様。
def for_else(id):
...
for item in items:
if item.id == id:
break
else:
item = create_item()
item.id = id
something_to_work_with(item)
...
例えば上記は条件に合うアイテムをループで探して、見つかればbreak
でループを離脱、見つからなければ新しく作るというコード。他の言語だとこれをやるために別にフラグ変数を用意したりすることもあるが、Pythonではこのようにシンプルに書ける。
(これは例なので全く同じことをやりたいだけなら内包表記とかもっと良い書き方がある。)
try-except-else 文
try
やexcept
の後ろにelse
句をつけると、try
の中で例外が発生しなかった場合のみ実行してくれる。Pythonではexcept
が例外発生有りの場合、else
が発生無しの場合、そしてfinally
が発生の有無に関わらず実行という3種の処理を定義できることになる。
def try_except_else():
...
try:
a = try_get_a()
except SomeError:
a = DEFAULT_A
else:
a = some_conversion(a)
something_to_work_with(a)
...
例えば上記はtry_get_a()
が成功した場合のみ、その返り値に対しsome_conversion
を適用するコード。
一見以下と同じだが、以下とは異なりtry
の中が1行と最小限になっていてより優れたコードだと言える。なぜなら下記のコードだとSomeError
がtry_get_a
で起きたのかsome_conversion
で起きたのか分からないから。設計上some_conversion
ではSomeError
は発生しないとしても、バグにより実は発生していてその例外を握りつぶしていた、なんて事が起こりうるので一般にtry
の中は最小限にすべきである。
try:
a = try_get_a()
a = some_conversion(a)
except SomeError:
a = DEFAULT_A
特に、独自に定義した例外をexcept
する場合はまだ良いが、Python標準の例外(ValueError
、KeyError
など)をexcept
する場合、try
の中は基本的には1行にした方が良い。標準の例外は様々な場所で発生しうるので、想定しない場所で発生した例外を意図せず握りつぶさないようにするためである。
raise-from 文
raise SomeError() from e
のようにraise
の後ろにfrom
をつけると例外の原因となった例外を設定できる。この原因となった例外の連鎖はトレースバックにも表示されるようになる。
class ItemNotFoundError(Exception):
pass
items = { 'a': itema, 'b': itemb }
def get_item(id):
try:
return items[id]
except KeyError as e:
raise ItemNotFoundError() from e
get_item('c')
例えば以下のようなトレースバックが表示される。
Traceback (most recent call last):
File "raise_from.py", line 11, in get_item
return items[id]
KeyError: 'c'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "raise_from.py", line 26, in <module>
get_item('c')
File "raise_from.py", line 13, in get_item
raise ItemNotFoundError() from e
__main__.ItemNotFoundError
ItemNotFound
の原因となったKeyError
の発生箇所までちゃんと表示してくれている。
例外の連鎖は例外の設計までちゃんと行なっていれば自然と必要になってくる。例えば上記のコードでget_item
関数がtry-exceptを一切せずに、ItemNotFoundError
ではなくKeyError
を直接投げたらどうだろう?get_item
がdict
型を使ってIDの管理をしているという実装詳細が外に漏れている。例えば後々この部分がデータベースになったらまた別の例外が投げられることになり、せっかくget_item
という名前で処理を抽象化しているのにそれが中途半端になってしまう。
例外の連鎖の鎖はデバッグの際のために記憶しておいた方が良い。鎖を記憶しておかないと一番上の例外の発生点からのスタックトレースしか得られず、デバッグがやりづらくなる。全ての鎖を記憶しておけば根本的な例外の発生原因にすぐにたどり着ける。
多くの言語では原因例外を例外のコンストラクタで渡すことで記憶することが多い(throw SomeError("Message", e);
のような感じ)が、これにはデメリットがある。独自例外を定義する者はコンストラクタを定義するとき自分の責任で原因例外を渡せるようにしなければならないという点である。もし原因例外を渡せるような作りになっていなければ、使用者側ではどうしようもない。
例外の連鎖を例外システムの一部であると捉え言語構文として導入しているPythonはよく考えられていると思う。
感想
どれも超便利なので他の言語も真似して導入して欲しいのですが、全然真似してくれない...
互換性を崩さず簡単に導入できそうなんですけどねぇ。