はまったこと
Python のコレクションの要素に累算代入演算子を使ったところ、想定外の動作をしたので、メモしておきます。
デフォルトの設定を保持する辞書 default_settings
に追加の設定 additional_settings
をマージしてから特定の設定をいじる処理をしていました。
>>> # 以下の2変数は変更したくない(定数的なものを想定)
>>> default_settings = {'a': 1, 'b': [0, 1]}
>>> additional_settings = {'a': 10, 'c': 'hoge'}
>>>
>>> settings = dict(default_settings, **additional_settings)
>>> settings
{'a': 10, 'b': [0, 1], 'c': 'hoge'}
>>>
>>> # 'list' に 2, 3 を追加
>>> # default_settings が変更されたら困るので extend でなく、+ 演算子を使う
>>> settings['b'] += [2, 3]
>>> settngs # こっちは OK
{'a': 10, 'b': [0, 1, 2, 3], 'c': 'hoge'}
>>> default_settings # こっちは NG。なぜか 'b' が書き換っている!!
{'a': 1, 'b': [0, 1, 2, 3]}
なぜそうなるか
Python のドキュメントを見ると以下のように書いてあります。
x += 1
のような累算代入式は、x = x + 1
のように書き換えてほぼ同様の動作にできますが、厳密に等価にはなりません。累算代入の方では、x
は一度しか評価されません。また、実際の処理として、可能ならば インプレース (in-place) 演算が実行されます。これは、代入時に新たなオブジェクトを生成してターゲットに代入するのではなく、以前のオブジェクトの内容を変更するということです。
つまり、上の例ではあえて extend
でなく +
としたが、+=
と書くと +
ではなく extend
と同じ動作をするということです。よって正しくは以下のようにしなくてはいけません。
>>> default_settings = {'a': 1, 'b': [0, 1]}
>>> additional_settings = {'a': 10, 'c': 'hoge'}
>>>
>>> settings = dict(default_settings, **additional_settings)
>>> settings
{'a': 10, 'b': [0, 1], 'c': 'hoge'}
>>>
>>> settings['b'] = settings['b'] + [2, 3]
>>> settngs
{'a': 10, 'b': [0, 1, 2, 3], 'c': 'hoge'}
>>> default_settings
{'a': 1, 'b': [0, 1]}
何、その仕様???
ちなみに C, C++, Java, C#, Ruby などを調べましたが、こんな奇妙な仕様にはなっていないようです。(ただし、Ruby のメンバに対する =
は代入(メンバへのバインド)ではなくメソッド呼び出しなので、Ruby だけちょっと事情が異なりますが。)