naritoブログ

このブログはDjangoとBootstrap4で作成されました
Githubのソースコード

Pythonの、変数と代入についての誤解を解く

約153日前 2017年11月3日20:10
プログラミング関連
Python

通常の代入では、オブジェクト自体は変更されない


先に、よくある誤解をといておきましょう。
以下のコードは、オブジェクトそのものを変更する動作は何もありません。コピーもしません。
  1. >>> a = [1, 2, 3, 4, 5]
  2. >>> b = a
  3. >>> a = []


Python公式ドキュメント日本語訳に、非常に簡潔でわかりやすい一文があったので引用します。


Python において代入文はオブジェクトをコピーしません。代入はターゲットとオブジェクトの間に束縛を作ります。


この束縛、という単語を借りて説明するならば...
変数bに変数aを代入するならば、「変数b」と、「変数aが指しているオブジェクト」の間に束縛を作りますし
変数aに100という数値を直接代入するなら、メモリ内に100なオブジェクトを作り、束縛を作ります。※1
変数aに直接[1, 2, 3]というリストを代入する場合も、同じです。
代入はミュータブル・イミュータブル問わず、コピーする訳ではありません。通常の代入に関しては、両者のやっていることは同じです。
既にある変数名を上書きしても、これは同じです。左辺の変数名と、右辺のオブジェクトの間に新しく束縛を作るだけです。


「通常の代入」と書きました。こう書いたのは理由がありますが、後で説明します。

※1
小さな数値やちょっとした文字列は、既にあるオブジェクトを再利用することがあります。


先程のコードを解説します。
a = [1, 2, 3, 4, 5] で、[1, 2, 3, 4, 5]というリストのオブジェクトが新しく作成されます。
そしてaという名前と、今作った[1, 2, 3, 4, 5]の間に束縛を作ります。
  1. a = [1, 2, 3, 4, 5]



bと、aが指している[1, 2, 3, 4, 5]の間に束縛を作ります。メモリのどっかに作られた[1, 2, 3, 4, 5]の視点から見ると、aとbが自分を見ています。
  1. b = a



[]というリストのオブジェクトを新しく作り、aという名前との間に束縛を作ります。
[1, 2, 3, 4, 5]から見ると、aは別の人を見るようになりました。今はbだけが自分を見つめています。
元の[1, 2, 3, 4, 5]というリストには何もしません。※2
  1. a = []


※2
誤解のないように説明すると、代入自体では何もしないのですが、このコードにb=aがなかった場合は、[1, 2, 3, 4, 5]はガベージコレクションされ回収されます。
ただ、それを気にする必要はないです。わざわざ説明する必要もなかったなと今では思います。


先程の例はリストでしたが、イミュータブルな数値の例でも説明します。
  1. >>> a = 1
  2. >>> b = a
  3. >>> a = 2



1なオブジェクトが新しく作成されます。
そしてaという名前と、今作った1の間に束縛を作ります。
  1. a = 1



bと、aが指している1の間に束縛を作ります。1の視点から見ると、aとbが自分を見ています。
  1. b = a



2なオブジェクトを新しく作り、aという名前との間に束縛を作ります。
1から見ると、aは別の人を見るようになりました。。今はbだけが自分を見つめています。
  1. a = 2



イミュータブルなオブジェクトを代入しても、値をコピーする訳ではない


イミュータブルを代入した場合、値をコピーするんじゃないの?と思う方もいるかもしれませんが、この誤解をといておきます。
イミュータブルなくせに、ミュータブルを格納できるタプルを使うのが一番わかりやすいです。
  1. >>> a = (1, 2, 3, [4, 5])
  2. >>> b = a




タプル内のリスト(正確にはミュータブル)は、それ自体の変更はできてしまいます。(こういう風にミュータブルを入れると、辞書のキーにできないのも覚えておくと良いです)
これでa[3].appendとして変更してみます。値をコピーしているのならば、bの中身は変わらないはずですね?
しかし、中身を見るとbも明らかにappendの影響を受けています。
イミュータブルであろうと、a=bとしたら、aとbが指しているものは同一なのです。id関数や、is演算子で確認しても同一ということがわかります。
  1. >>> a[3].append(100)
  2. >>> a
  3. (1, 2, 3, [4, 5, 100])
  4. >>> b
  5. (1, 2, 3, [4, 5, 100])



a += [2] は、通常の代入とは違う


冒頭で通常の代入通常の代入、とうるさかったのはこれを説明するためです。

以下のような、+=は通常の代入と分けて考えた方が良いケースがあります。
  1. a = [1]
  2. a += [2]



これをa = a + [2]と解釈すると、今までの前提が狂ってきます。違うのです、+=、とくにミュータブルでこれを呼び出した際の挙動が特殊なのです。
a += [2]ですが、これは以下と同義です
  1. a = a.__iadd__([2])



やっぱり代入に見えます。確かに代入なのですが、iaddでやっているのはextendと似た処理です。自身にリストを追加しています。
そもそも右辺のiaddで自身を変更しており、Noneなんか返すとまずいので、自身をまた返却している、ということです。
代入で自身を変更しているというよりは、代入前の処理で自身が変更されているのが真相です。
無理やり複数行に書くとこんな感じです。
  1. a.extend(2)
  2. a = a



私の個人的な感想ですが、変数や代入を説明する際に、+=を安易に使用するのはお勧めしません。
ミュータブルなオブジェクトでは、上記のようにiaddが特殊な動作をすることが多いからです。(iaddのiは、そもそもインプレースのiです)
場合によっては、代入処理の本質が見えなくなる可能性もあります。


お勧めのサイト


Pythonの変数・オブジェクトがどのように作成され、どのように参照されているかに困ったときは以下のサイトがお勧めです。
Python Online Tutor
http://pythontutor.com/live.html#mode=edit

ボタンを押していくと、その動きを図で確認できます。同一なオブジェクトもちゃんと表示されるため、非常にわかりやすいです。