メモ帳

python, juliaで機械学習をやっていく

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を使う