LoginSignup
7
9

More than 5 years have passed since last update.

Python の累算代入は演算してから代入しているのではない

Posted at

はまったこと

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 だけちょっと事情が異なりますが。)

7
9
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
9