Pythonでlistをコピーしたいだけなのに、調べたら方法がたくさんあったという話
これはなに
Pythonでlistをコピーする方法について調べたメモ。
結論 | 結局どの手法が良いのか
どの方法が優れているかは状況によるが、下記の調査結果から、筆者は下記の手法を選択しようと思った。
- 浅いコピー :
copy.copy()を利用 - 深いコピー :
copy.deepcopy()を利用
はじめに
Pythonでは、b = aでリストをコピーできない。
b = aでリストをコピーしようとすると、新しいリストが作成されるのではなく、既存のリストaへの新しい参照bが作成される。つまり、aとbは同じオブジェクトを指す。
a = [1, [2, 3], 4]
b = a
# id()はオブジェクトのメモリ上のアドレスを返す
print(id(a) == id(b)) # True
そのため、aまたはbのいずれかを変更すると、もう一方も変更される。
a = [1, [2, 3], 4]
b = a
print(id(a) == id(b)) # True
a[0] = 99
print(a) # [99, [2, 3], 4]
print(b) # [99, [2, 3], 4]
コピーの種類
Pythonには、下記2種類のコピーがある。
- 浅いコピー
- 深いコピー
浅いコピー
浅いコピーとは、ネストされたリストやオブジェクト(e.g. リスト内のリスト)の外側のレベルだけを新しいオブジェクトとしてコピーし、内側のオブジェクトは参照としてコピーすることを指す。
そのため、コピーされたリストのネストしたオブジェクトが変更されると、元のリストも影響を受ける。
original_list = [1, [2, 3], 4]
copied_list = original_list.copy() # 浅いコピー
# 外側のレベルを変更
copied_list[0] = 77
# 外側のレベルは新しいオブジェクトとしてコピーされるため、
# 浅いコピーされたオブジェクトだけ値が変わる
print(original_list) # Output: [1, [2, 3], 4]
print(copied_list) # Output: [77, [2, 3], 4]
# ネストされたリストの内部の値を変更
copied_list[1][0] = 99
# 内側のオブジェクトは参照としてコピーされるため
# 元のリストも影響を受ける
print(original_list) # Output: [1, [99, 3], 4]
print(copied_list) # Output: [77, [99, 3], 4]
listの浅いコピーの作成方法
浅いコピーを作成する方法は下記のとおりである。
- スライス
[:]の利用 -
copyメソッドの利用 -
list()コンストラクタの利用 -
copy.copy()の利用
スライス[:]
Pythonのスライスを利用する。スライスは新しいオブジェクトを作成するため、浅いコピーを作成できる。
original_list = [1, 2, 3, 4, 5]
copied_list = original_list[:]
長所
- 読みやすくシンプル
- 外部モジュールをインポートする必要がない
短所
- これがコピーを作成する操作であると即座に理解しにくい
- とくに、新人や他のプログラム言語から来た開発者にとってはわかりにくい
copyメソッド
オブジェクトの持つcopyメソッドを利用する。copyメソッドは新しいオブジェクトを作成するため、浅いコピーを作成できる。
original_list = [1, 2, 3, 4, 5]
copied_list = original_list.copy()
長所
- 直感的でわかりやすい
- 特定の型に対する一般的なコピーメソッドとして、他のプログラミングコンテキストでも広く認識されている
短所
- 一部の組み込みオブジェクトやユーザー定義クラスでは利用できない場合がある(
copyメソッドを実装していない場合)
list()コンストラクタ
list()コンストラクタを利用する。list()コンストラクタは新しいオブジェクトを作成するため、浅いコピーを作成できる。
original_list = [1, 2, 3, 4, 5]
copied_list = list(original_list)
長所
- ユーザー定義クラスなどで明示的なコピー方法が提供されていない場合でも動作する
- メソッド呼び出しではなくコンストラクタのため
- 関数的で、一般的なコピー手法として他の型にも適用可能である可能性がある
短所
- 具体的に「コピーを作成する」ことを表現していないため、コードを初めて読む人にとって直感的でない場合がある
copy.copy()の利用
copyモジュールをインポートし、copy.copy()を利用する。copy.copy()は新しいオブジェクトを作成するため、浅いコピーを作成できる。
import copy
original_list = [1, 2, 3, 4, 5]
copied_list = copy.copy(original_list)
長所
- モジュール名copyがコードに含まれているため、この操作がコピーを作成していることをはっきりと示している
- 他のオブジェクトタイプ(e.g. ユーザー定義クラス)でも同じ関数を使用でき、一貫性がある
短所
- モジュールをインポートする必要がある
- 他の手法に比べてやや冗長
深いコピー
深いコピーとは、元のオブジェクトの完全な複製を作成することを指す。ネストしたすべてのオブジェクトもコピーされ、元のオブジェクトと完全に独立する。すなわち、ネストされたリストやオブジェクト(e.g. リスト内のリスト)のすべてのレベルを新しいオブジェクトとしてコピーする。
そのため、コピーされたリストのネストしたオブジェクトが変更されても、元のリストは影響を受けない。
import copy
original_list = [1, [2, 3], 4]
copied_list = copy.deepcopy(original_list) # 深いコピー
# ネストされたリストの内部の値を変更
copied_list[1][0] = 99
# 内側のオブジェクトも新しいオブジェクトとしてコピーされるため
# 元のリストは影響を受けない
print(original_list) # Output: [1, [2, 3], 4]
print(copied_list) # Output: [1, [99, 3], 4]
listの深いコピーの作成方法
一般的なケースに対して深いコピーを作成する方法は下記のとおりである。
-
copy.deepcopy()の利用
copy.deepcopy()の利用
copyモジュールをインポートし、copy.deepcopy()を利用する。copy.deepcopy()は深いコピーを作成できる。
import copy
copied_list = copy.deepcopy(original_list)
listの深いコピーを作成する他の方法
listの深いコピーを作成一般的に確立された方法は、copy.deepcopy()を利用することである。しかし、他の方法も考えられはする。
リスト内包表記と再帰を使う方法
複製したいリストに別のリストが含まれている場合(ネストされている場合)、リスト内包表記を使いながら再帰関数を適用して、各ネストしたリストを個別にコピーする方法が考えられる。ネストの深さや構造が単純な場合には有用である。
def deep_copy(lst):
return [deep_copy(item) if isinstance(item, list) else item for item in lst]
original = [1, [2, [3, 4], 5], 6]
copied = deep_copy(original)
pickleモジュールのloads()とdumps()の利用
pickleモジュールをインポートし、loads()とdumps()を利用する。
import pickle
original_list = [1, [2, [3, 4], 5], 6]
copied = pickle.loads(pickle.dumps(original_list))
jsonモジュールのloads()とdumps()の利用
jsonモジュールをインポートし、loads()とdumps()を利用する。この手法はオブジェクトがJSONで表現可能な型(リスト、辞書、文字列、整数、浮動小数点数、True、False、None)のみを含む場合にのみ機能する。
import json
original_list = [1, [2, [3, 4], 5], 6]
copied_list = json.loads(json.dumps(original_list))
終わりに | 感想
- パッと見ただけではコピーしているように見えない手法もあることを知れてよかった
-
copyモジュールは偉大
Discussion
ありがとうございます。ためになりました。
jsonのソースがpickleと同じになっています
ご指摘ありがとうございます!修正しました