Ruby
Python

PythonとRubyでの「初期化していないローカル変数」の取り扱いについて

はじめに

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になるので良い子は真似しちゃダメです。