Pythonでリストをコピーする際の注意点

f:id:monozukuri-bu:20190226205956j:plain

みなさま、枕の下には何を置いていますか?
私は、Pythonのライブラリーリファレンスを置いて寝ています。
Kibaraです。

意味が分からなかった方はこちら。

3.6.5 Documentation

今回は、Python初心者が陥りやすいリストのコピーについてまとめます。

値の代入

早速ですが、以下のコードを実行するとどんな結果になるでしょうか。

a = 3
b = a

print("a=", a) # a= 3
print("b=", b) # b= 3

a = 5
print("a=", a) # ???
print("b=", b) # ???

当然、結果はこうなります。

a= 5
b= 3

リストの代入

では、リストではどうでしょうか。

a = [1, 2, 3, 4, 5]
b = a

print("a=", a) # a= [1, 2, 3, 4, 5]
print("b=", b) # b= [1, 2, 3, 4, 5]

a[0] = 10
b[1] = 20
print("a=", a) # ???
print("b=", b) # ???

結果は、こうです。

a= [10, 20, 3, 4, 5]
b= [10, 20, 3, 4, 5]

意図した結果になりませんでした。
なぜでしょうか。

オブジェクトの同一性

全てのオブジェクトは、同一性(id)、型(type)、値(value)を持っています。
確認してみましょう。

a = [1,2,3,4,5]

# 同一性
print(id(a))   # 1697132501064

# 型
print(type(a)) # <class 'list'>

# 値
print(a)       # [1, 2, 3, 4, 5]

このidは、CPythonでは値が格納されているメモリのアドレスを返却します。
つまり同一性という言葉の通り、idの値が同じなら同じオブジェクトであるということを意味しています。

先ほどの例でidの値を調べてみましょう。

# 整数の場合
a = 3
b = a

print(id(a)) # 1743552624
print(id(b)) # 1743552624

a = 5
print(id(a)) # 1743552688 <- 変化した
print(id(b)) # 1743552624 <- そのまま

# リスト
c = [1,2,3,4,5]
d = c

print(id(c)) # 3173605562440
print(id(d)) # 3173605562440

c[0] = 10
print(id(c)) # 3173605562440 <- 変化しない
print(id(d)) # 3173605562440 <- そのまま

整数型の場合、b=aの時点ではidは同じですが、a=5を行うとaのidが変化しました。
これは値の変更というよりは、新しいオブジェクトとして再束縛(rebind)しているためです。
こうした動作を行うオブジェクトを、immutableと呼びます。

対してリスト型の場合、c[0] = 10としてもidが変化しませんでした。
これは、コンテナの中の一部の値を"変更"する操作になっているためです。
こうした動作を行うオブジェクトは、mutableと呼びます。

そのオブジェクトがimmutableかmutableなのかは、型によって決まります。

リストのコピー

参照元に影響を与えずにリストをコピーするには、slice()かcopyモジュールがオススメです。

スライスは、範囲を切り取って新しく作ったオブジェクトを返します。
copyモジュールは、オブジェクトをコピーする機能を提供します。

Python標準ライブラリ - Slice
Python標準ライブラリ - Copy

a = [1,2,3,4,5]
print(id(a)) # 2427214288968

# スライス
b = a[:]
b[0] = 10
print(id(b)) # 2427214289096

# copyモジュール
import copy
c = copy.copy(a)
c[1] = 60
print(id(c)) # 2427215989640

print(a) # [1, 2, 3, 4, 5]
print(b) # [10, 2, 3, 4, 5]
print(c) # [1, 60, 3, 4, 5]

きちんとidが変わりました。
コピー後に値を変更しても他には影響なさそうです。

多次元配列

これまで1次元のリストで例を見てきました。
多次元だとどうなるでしょうか。

a = [[1,2,3,4,5],
    [6,7,8,9,10],
    [11,12,13,14,15]]
print(id(a)) # 2279782197448

# スライス
b = a[:]
b[0][0] = 10
print(id(b)) # 2279782197576

# copyモジュール
import copy
c = copy.copy(a)
c[1][0] = 60
print(id(c)) # 2279782206536

print(a) # [[10, 2, 3, 4, 5], [60, 7, 8, 9, 10], [11, 12, 13, 14, 15]]
print(b) # [[10, 2, 3, 4, 5], [60, 7, 8, 9, 10], [11, 12, 13, 14, 15]]
print(c) # [[10, 2, 3, 4, 5], [60, 7, 8, 9, 10], [11, 12, 13, 14, 15]]

あれー?
idの値は変わっているのに、コピー後の変更が他にも影響を与えてしまっています。

これは、浅いコピー(Shallow Copy)という動作になってしまっているためです。
外側のオブジェクトは確かにidが変わって別オブジェクトになっているのですが、
その中身はコピー元を参照しています。

a = [[1,2,3,4,5],
    [6,7,8,9,10],
    [11,12,13,14,15]]
print("=== a ===")
for a_row in a:
    print(id(a_row))

# スライス
b = a[:]
print("=== b ===")
for b_row in b:
    print(id(b_row))

# copyモジュール
import copy
c = copy.copy(a)
print("=== c ===")
for c_row in c:
    print(id(c_row))

これを実行すると、a,b,cのいずれもが同じ値を示します。

=== a ===
2479189710920
2479189711048
2479191402952
=== b ===
2479189710920
2479189711048
2479191402952
=== c ===
2479189710920
2479189711048
2479191402952

この浅いコピーの挙動については、ドキュメントのcopyモジュールの項目に記述されています。

浅いコピー (shallow copy) は新たな複合オブジェクトを作成し、 その後 (可能な限り) 元のオブジェクト中に見つかったオブジェクトに対する 参照 を挿入します。

copy.copy(x)

この問題を解決するには、copy.deepcopy()を使用すればOKです。

a = [[1,2,3,4,5],
    [6,7,8,9,10],
    [11,12,13,14,15]]
print("=== a ===")
for a_row in a:
    print(id(a_row))

# copyモジュール
import copy
b = copy.deepcopy(a)
b[0][0] = 10
print("=== b ===")
for b_row in b:
    print(id(b_row))

print("a=", a)
print("b=", b)

これを実行すると、次のような結果となります。

=== a ===
1524121524296
1524121524424
1524123216392
=== b ===
1524123238472
1524123222920
1524123238664
a= [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15]]
b= [[10, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15]]

ちなみに、どうしてもcopyモジュールを使いたくない場合は、
以下のような力技も可能です。

c = [row[:] for row in a]

(補足) リストをメソッドに渡すとき

いままでの例では単純にリストを別の変数にコピーする場合でしたが、
メソッドへリストを渡す場合も同様の問題が発生します。

# 配列を引数に取るメソッドを定義
def array_test(array):
    array[0] = 10
    print(id(array)) # 2350634948680
    print(array)     # [10, 2, 3, 4, 5]

# 配列を初期化してメソッドへ渡す
a = [1,2,3,4,5]
array_test(a)

# idが同一、値も変わってしまう
print(id(a)) # 2350634948680
print(a)     # [10, 2, 3, 4, 5]

こちらも解決策は同様で、きちんとコピーしてあげればOKです。

import copy

# 配列を引数に取るメソッドを定義
def array_test(array):
    array[0] = 10
    print(id(array)) # 2604483127624
    print(array)     # [10, 2, 3, 4, 5]

# 配列を初期化してdeepcopyしたものをメソッドへ渡す
a = [1,2,3,4,5]
array_test(copy.deepcopy(a))

# 値は変わらない
print(id(a)) # 2604483127688
print(a)     # [1, 2, 3, 4, 5]

まとめ

意識していないと陥りやすい罠として、リストのコピーを紹介しました。
今回のような事例は、一見すると動いているのになんだか挙動がおかしいという、
不可解なバグになりかねないので、注意が必要ですね。