はじめに
Pythonで以下のようなプログラムを書いたら例外(UnboundLocalError)が発生しました。
def foo(a):
if a > 0:
bar = 1
print(bar)
foo(-1)
まあそうだよね、というところではあるのですが、一方Rubyでは例外にはなりません。
def foo(a)
if a > 0
bar = 1
end
p bar
end
foo(-1) # => nil
Python的にもローカル変数barの領域は確保してあるだろうしNoneでいいんじゃない?というところですがそうにはならないようです。
実装を見てみる
CPython、CRubyそれぞれでローカル変数がどう初期化されているかを確認します。
CPython
関数実行情報を入れるPyFrameObject、それを作成しているPyFrame_New関数(Objects/frameobject.c)のうちローカル変数初期化に関する部分だけ抜き出すと、
extras = code->co_nlocals + ncells + nfrees;
f->f_valuestack = f->f_localsplus + extras;
for (i=0; i<extras; i++)
f->f_localsplus[i] = NULL;
詳しい説明は省きますがf_localsplusがローカル変数に使われている領域です。NULLで初期化されています。Noneじゃないんですね。(NULLとNoneは別物です。C実装的にはNoneはPy_Noneというれっきとしたオブジェクトです)
CRuby
一方、CRubyです。vm_insnhelper.cのvm_push_frame関数でローカル変数を初期化しています。
/* initialize local variables */
for (i=0; i < local_size; i++) {
*sp++ = Qnil;
}
nilを入れていますね。というわけで「変数に初めて代入される個所」を通らなくてもnilが入っているということになります。
言語のスタンスの違い
先にRubyを学んで後からPythonを学んだ人間なので、「辞書で指定されたキーがないと例外」「リスト範囲外に代入しようとすると例外」というPythonの挙動に対し「いやそこはNone返してよ」「よしなにリスト拡張してよ」と思ったものですがPythonに慣れてくると「これはおかしな状況だし例外ぽーん」というPythonのスタンスもさもありなんと思うようになってきました。
今回の「初期化していないローカル変数」についても処理系実装知ってる分(&Rubyの動作を考えると)「こう書けるだろう」としてましたが確かに「おかしな」状況です。
では、Pythonの動作とRubyの動作どちらが望ましいかというとまあどちらもありであり、言語設計者の思想の違いかなと思いました。個人的にはグイドさんもまつもとさんも好きです。
なお、個人的にはNoneで初期化されててくれる方がうれしいです:-P
余談
そもそもなぜ件のif文を書いたかですが、例外処理にて、
bar = None
try:
bar = some_func()
except:
pass
print(bar)
みたいなコードが書いてあったのを、先頭行の「bar = None」消しても大丈夫なんじゃないの?(Javaとかみたいに例外処理の外側で変数初期化するのなんかダサい)→大丈夫だったということでやってしまったということになります。ちなみにこれsome_funcで例外起きると最終行のbar参照は例によってのUnboundLocalErrorになるので良い子は真似しちゃダメです。