pythonの参照の考え方に頭を抱えた話

前提

古典ギリシャ語のテキストを表すには、大量のダイアクリティカルマークが必要になります(Polytonic Greekといいます)。今でこそUnicodeがあるおかげで表示自体は難なくできるけれども、昔はそんなことができる時代でもなかったし、そもそも今だって入力することは容易でないし、そういうわけだから、ASCII文字でギリシャ文字を表現しましょう、というのでBeta Codeというのが考案されました。例えばπροϊέναιであればPROI+E/NAIといった具合に表記します。今回、Beta CodeからPolytonic Greekに変換するプログラムを組みたいと思い、なんかいい感じのないかな~と、こちらからあるPythonコードを拾ってきたわけなのでした。

問題

当然一発ではうまく動かないので、いろいろ吟味してみたところ、奇妙な挙動のメソッドにぶちあたりました。

class Trie:
    def __init__(self):
        self.root = [None, {}]

    def add(self, key, value):
        curr_node = self.root
        for ch in key:
            curr_node = curr_node[1].setdefault(ch, [None, {}])            
        curr_node[0] = value

Trieクラスは、メンバ変数rootにBeta Codeとギリシャ文字の対応表をaddメソッドで格納し、変換等の手続きを行うものなのですが、問題はaddメソッド内での諸々の変数の挙動であります。例えば、「a)/ – ἄ」という対応を格納してみようと思います。

>>> t = Trie()
>>> t.add('a)/', 'ἄ')
>>> t.root
[None, {'a': [None, {')': [None, {'/': ['ἄ', {}]}]}]}]

これ、どうしてこうなったか皆さん分かりますか?私は最初全然わかりませんでした。とりあえずこれだけの疑問が出てきました。

  1. curr_nodeにしか代入操作をしていないのに、なんでt.rootが書き換わっているのよ
  2. curr_node[1](これはコンストラクタを見ればdict型とすぐわかります)のsetdefaultメソッド(辞書に第一引数のキーが無ければ、「第一引数:第二引数」という要素を追加するメソッド)を繰り返して、なんでこんな入れ子構造のリストができるんねん
  3. 最後にcurr_node[0] = valueってやってるのに、t.rootの奥底にἄが代入されているのはなぜなのよ

皆さん分かりますでしょうか。

答え合わせ

1.は至極単純な話で、pythonの代入演算子は、右オペランドが変数やオブジェクトだと参照渡しを行います1この表現もあまり正確ではないみたいですが、説明できる能力がないので割愛します。。なので、curr_node = self.rootとした時点で、以後curr_nodeに対する操作はそのままt.rootに対する操作になるわけです。ここまではまあいいでしょう。

2.が厄介。for ch in key:でキーの元となるa)/をバラバラにしてchに格納するところまでは良いとして、問題は次の文です。

curr_node = curr_node[1].setdefault(ch, [None, {}])

ここでsetdefaultメソッドは、辞書curr_node[1]すなわちt.root[1]へ要素'a': [None, {}]の追加が無事に行われれば、第二引数、つまり[None, {}]を返します。ですので、これがそのままcurr_nodeに代入されるわけです。1回目のループが終わった時点での各変数の値を見てみましょう。

>>> t.root
[None, {'a': [None, {}]}]
>>> curr_node
[None, {}]

ここまでは予想通りです。しかしそれにしても、なぜcurr_nodeにわざわざ同じ値[None, {}]を代入しているのでしょうか。まあそれはとりあえず置いといて、2回目のループを終えてみましょう。

>>> t.root
[None, {'a': [None, {')': [None, {}]}]}]
>>> curr_node
[None, {}]

ここから奇妙な入れ子構造が出現しました。どうやら、要素の追加はキー’a’の値[None, {}]内の辞書に対して行われているようです。どうしてこんなことになってしまったのでしょうか。

実は先程「値[None, {}]を代入」と書いたのは誤りで、リストは値でなくれっきとしたオブジェクトなんですね。ですから、正確には左辺のcurr_nodeにはオブジェクト[None, {}]への参照が格納されるわけです。さらに、setdefaultメソッドで辞書curr_node[1]に格納される要素'a': [None, {}]の値[None, {}]も、実のところ前述したのと同じオブジェクト[None, {}]への参照なんです。

そういうわけなので、2回目のループでsetdefaultメソッドが行う要素追加は、curr_node[1]つまりさっき出てきたオブジェクト[None, {}]の第2要素{}に対して行われるわけです。そしてcurr_nodeにはオブジェクト[None, {}](今まで出てきたものとは別物です!)への参照が格納され、というのを繰り返した結果、上記のような入れ子構造になるわけです。んなの分かるか!!!?!?!!

ここまでいけば3.もすんなり理解できます。curr_node[0] = valueという代入操作は、結局、一番奥深くにある[None, {}]の第一要素になされるのです。そして最初に見たような実行結果が得られるわけです。

とまあこんな具合にしてpythonの参照というのは中々奥が深いわけですが、それにしてもどうしてこんな仕様になっているのか、あとこのプログラマーもどうしてまあこんな離れ業を使ったのか、とため息を出さずにはいられませんでした。もしこの技術が割とメジャーだとしたらどなたか教えてください。よろしくお願いいたします。

 

カテゴリーIT

コメントを残す

メールアドレスが公開されることはありません。