pythonの参照渡しでハマらないために変数への代入時のメモリの使い方について調べる
pythonの参照渡しでハマらないために変数への代入時のメモリの使い方について調べる
ハマった例
a = [1, 2] b = a b[0] = 5 print(a) # >>> [5, 2]
参照渡しが行われるので代入元の変数の値もかわってしまう。これに、初学者は一度はハマると思う。
対処法
挙動について調べる前に対処法を先に書くと、copy関数を使えば参照渡しでなく、新たにメモリ領域が確保されるので上記のような問題はなくなる。
import copy a = [1, 2] b = copy.copy(a) # aが多重リストや辞書型を要素に持つ場合はcopy.deepcopy(a)を使う b[0] = 5 print(a) # >>> [1, 2]
本題
変数を変数に代入したとき
変数に値を代入したものを別の変数に代入したとき、以下のように同じ値が入った別のメモリ領域が確保されるわけではなく、代入元の値が格納されているメモリ領域の参照が渡される。
id(1) >>> 4300186960 # 代入時に値1は新たにメモリ領域がかくほされるわけではない a = 1 print(f'a: {a}, {id(a)}') # >>> a: 1, 4300186960 # さらに変数に代入しても同様 b = a print(f'a: {a}, {id(a)}') print(f'b: {b}, {id(b)}') # >>> a: 1, 4300186960 # >>> b: 1, 4300186960
数値型だけでなく、文字列型、リスト型、辞書型も同様である。ただし、リスト型と辞書型は直接代入時に新たにメモリ領域を確保するようである。
# 文字列型の代入 id("aaa") # >>> 4346716208 a = "aaa" print(f'a: {a}, {id(a)}') # >>> a: aaa, 4346716936 b = a print(f'a: {a}, {id(a)}') # >>> a: aaa, 4346716936 print(f'b: {b}, {id(b)}') # >>> b: aaa, 4346716936 # リスト型の代入 id([1, 2, 3]) # >>> 4345910984 a = [1, 2, 3] print(f'a: {a}, {id(a)}') # >>> a: [1, 2, 3], 4345551560 b = a print(f'a: {a}, {id(a)}') # >>> a: [1, 2, 3], 4345551560 print(f'b: {b}, {id(b)}') # >>> b: [1, 2, 3], 4345551560 # 辞書型の代入 id({"a": 1, "b": 2, "c":3}) # >>> 4347245552 ad = {"a": 1, "b": 2, "c":3} print(f'ad: {ad}, {id(ad)}') # >>> ad: {'a': 1, 'b': 2, 'c': 3}, 4347244688 bd = ad print(f'ad: {ad}, {id(ad)}') # >>> a: {'a': 1, 'b': 2, 'c': 3}, 4347244688 print(f'bd: {bd}, {id(bd)}') # >>> b: {'a': 1, 'b': 2, 'c': 3}, 4347244688
ただ、リスト型と辞書型が変数に直接代入時に新たにメモリを確保すると言っても、どの値がどの順番に入っているかなどの補足的な情報のために新たにメモリを確保しているだけであって、1などの値自体は共通のメモリを参照しているようである。
print(id(1)) # >>> 4300186960 print(id(a[0])) # >>> 4300186960 print(id(ad["a"])) # >>> 4300186960
以上のメモリの扱いの差異が参照渡しの混同を引き起こしている。
数値型と文字列型の再代入
以下のように数値型と文字列型の代入は値自体のメモリ領域のアドレスを渡していることがわかる。なので、変数を変数に代入しても依存関係はなく、後に他の値を代入しても代入元の変数には影響を与えない。
# 数値型の代入 a = 1 b = a b = 2 print(f'a: {a}, {id(a)}') # >>> a: 1, 4300186960 print(f'b: {b}, {id(b)}') # >>> b: 2, 4300186992 # 文字列型の代入 a = "aaa" b = a b = "bbb" print(f'a: {a}, {id(a)}') # >>> a: aaa, 4347219392 print(f'b: {b}, {id(b)}') # >>> b: bbb, 4309524696
リスト型や辞書型などのcollection型の再代入
問題が起きない場合: collection型の再定義
以下のようにcollection型そのものを再代入した場合には参照渡しの問題は起きずに、独立に値がメモリに格納される。
# リスト型の代入 a = [1, 2, 3] b = a b = [1, 2, 3] print(f'a: {a}, {id(a)}') # >>> a: [1, 2, 3], 4345534216 print(f'b: {b}, {id(b)}') # >>> b: [1, 2, 3], 4319872584 # 辞書型の代入 ad = {"a": 1, "b": 2, "c":3} bd = ad bd = {"a": 1, "b": 2, "c":3} print(f'ad: {ad}, {id(ad)}') # >>> ad: {'a': 1, 'b': 2, 'c': 3}, 4347049592 print(f'bd: {bd}, {id(bd)}') # >>> bd: {'a': 1, 'b': 2, 'c': 3}, 4346635536
問題が起きる場合: 要素の代入, 追加, 削除
しかし、要素を変更しようとすると参照渡しをしている弊害がでる
# リスト型の代入 a = [1, 2, 3] b = a b[0] = 2 b.pop() b.append(4) b += [5] print(f'a: {a}, {id(a)}') # >>> a: [2, 2, 4, 5], 4347420936 print(f'b: {b}, {id(b)}') # >>> b: [2, 2, 4, 5], 4347420936 # 辞書型の代入 ad = {"a": 1, "b": 2, "c":3} bd = ad del bd["c"] bd["d"] = 4 bd.update({"e": 5}) print(f'ad: {ad}, {id(ad)}') # >>> ad: {'a': 1, 'b': 2, 'd': 4, 'e': 5}, 4347443384 print(f'bd: {bd}, {id(bd)}') # >>> bd: {'a': 1, 'b': 2, 'd': 4, 'e': 5}, 4347443384
変数に変数を代入した後で要素を変更した場合、代入元の要素も変更されている。 リスト型と辞書型が変数に直接代入時にどの値がどの順番に入っているかなどの補足的な情報のために新たにメモリを確保しており、変数に変数を代入したときにこの補足的な情報が格納してあるメモリの参照が渡される。なので、変数に対する値や順番の変更は代入先と代入元で共通したメモリ領域を書き換えるので、両方の値がかわってしまう。 一方で、数値型や文字列型またはcollection型自体の再代入で参照渡しの問題が起こらない理由は、以下のように、aとbの両方の共通のメモリ領域自体を変更せずに参照先だけがかわっているためである。
# 数値型の代入 a = 1 b = a b = 2 print(f'a: {a}, {id(a)}') # >>> a: 1, 4300186960 print(f'b: {b}, {id(b)}') # >>> b: 2, 4300186992
まとめ
- 代入はどの型でも基本的に参照渡し
- 参照するメモリ領域自体を変更する処理の場合参照渡しの問題が起こり得る
- 主にcollection型の要素の変更に注意する
- collection型のコピーにはcopy moduleを使う