あまり知られていないけれど、便利な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はよく考えられていると思う。
感想
どれも超便利なので他の言語も真似して導入して欲しいのですが、全然真似してくれない...
互換性を崩さず簡単に導入できそうなんですけどねぇ。