前提
古典ギリシャ語のテキストを表すには、大量のダイアクリティカルマークが必要になります(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, {'/': ['ἄ', {}]}]}]}]
これ、どうしてこうなったか皆さん分かりますか?私は最初全然わかりませんでした。とりあえずこれだけの疑問が出てきました。
curr_node
にしか代入操作をしていないのに、なんでt.root
が書き換わっているのよcurr_node[1]
(これはコンストラクタを見ればdict型とすぐわかります)のsetdefault
メソッド(辞書に第一引数のキーが無ければ、「第一引数:第二引数」という要素を追加するメソッド)を繰り返して、なんでこんな入れ子構造のリストができるんねん- 最後に
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の参照というのは中々奥が深いわけですが、それにしてもどうしてこんな仕様になっているのか、あとこのプログラマーもどうしてまあこんな離れ業を使ったのか、とため息を出さずにはいられませんでした。もしこの技術が割とメジャーだとしたらどなたか教えてください。よろしくお願いいたします。