【Python入門】リストのコピー方法を完全解説|浅いコピーと深いコピーの違いとは?

python

「このリストをもう一つ同じものとしてコピーしたい」
a = bでコピーしたつもりなのに、なぜか両方とも変わってしまう…」

そんな疑問を解決するために、今回はリストのコピー方法について、初心者の方でも理解できるように詳しく説明していきます。

スポンサーリンク

まずは基本:「=」での代入はコピーではない

多くの初心者が陥りがちな間違いから見ていきましょう。

よくある間違い

# 間違ったコピー方法
original = [1, 2, 3, 4, 5]
copied = original  # これはコピーではない!

print("元のリスト:", original)  # [1, 2, 3, 4, 5]
print("コピー:", copied)        # [1, 2, 3, 4, 5]

# copied を変更してみる
copied[0] = 100

print("変更後の元のリスト:", original)  # [100, 2, 3, 4, 5] ← なぜか変わってしまう!
print("変更後のコピー:", copied)        # [100, 2, 3, 4, 5]

なぜこうなるのか?

original = [1, 2, 3, 4, 5]
copied = original

# 同じオブジェクトを指しているか確認
print("同じオブジェクト?:", original is copied)  # True
print("originalのID:", id(original))
print("copiedのID:", id(copied))  # 同じIDが表示される

pythonでは、リストはオブジェクトの参照(アドレス)で管理されています。

copied = originalは「copiedもoriginalと同じリストを指す」ことになります。
つまり、同じ箱を2つの名前で呼んでいるだけなのです。

図解:参照とコピーの違い

参照(=での代入):
original → [1, 2, 3, 4, 5] ← copied
          同じリストを指している

コピー(この記事でやりたいこと):
original → [1, 2, 3, 4, 5]
copied   → [1, 2, 3, 4, 5]  ← 別のリスト

浅いコピー(シャローコピー)の方法

「浅いコピー」とは、リストの1階層目だけをコピーする方法です。

方法1:スライスを使う(最もシンプル)

original = [1, 2, 3, 4, 5]
copied = original[:]  # 全体をスライス

# 変更して確認
copied[0] = 100
print("元のリスト:", original)  # [1, 2, 3, 4, 5] ← 変更されない
print("コピー:", copied)        # [100, 2, 3, 4, 5]

# 異なるオブジェクトか確認
print("異なるオブジェクト?:", original is not copied)  # True

方法2:list()関数を使う

original = [1, 2, 3, 4, 5]
copied = list(original)

copied[1] = 200
print("元のリスト:", original)  # [1, 2, 3, 4, 5] ← 変更されない
print("コピー:", copied)        # [1, 200, 3, 4, 5]

方法3:copy()メソッドを使う

original = [1, 2, 3, 4, 5]
copied = original.copy()

copied[2] = 300
print("元のリスト:", original)  # [1, 2, 3, 4, 5] ← 変更されない
print("コピー:", copied)        # [1, 2, 300, 4, 5]

方法4:copyモジュールのcopy()を使う

import copy

original = [1, 2, 3, 4, 5]
copied = copy.copy(original)

copied[3] = 400
print("元のリスト:", original)  # [1, 2, 3, 4, 5] ← 変更されない
print("コピー:", copied)        # [1, 2, 3, 400, 5]

どの方法を使えば良い?

方法特徴おすすめ度
[:]最もシンプル、読みやすい
list()明示的でわかりやすい
.copy()メソッドなので直感的
copy.copy()importが必要だが統一感がある

浅いコピーの限界:ネストされたリストの問題

浅いコピーには重要な制限があります。

リストの中にリストがある場合(ネストされたリスト)、内側のリストは共有されたままになってしまいます。

問題を実際に見てみる

# 2次元リスト(リストの中にリストがある)
original = [[1, 2], [3, 4], [5, 6]]
copied = original[:]  # 浅いコピー

print("コピー前:")
print("元のリスト:", original)  # [[1, 2], [3, 4], [5, 6]]
print("コピー:", copied)        # [[1, 2], [3, 4], [5, 6]]

# 内側のリストを変更してみる
copied[0][0] = 100

print("変更後:")
print("元のリスト:", original)  # [[100, 2], [3, 4], [5, 6]] ← なぜか変わってしまう!
print("コピー:", copied)        # [[100, 2], [3, 4], [5, 6]]

なぜこうなるのか?

original = [[1, 2], [3, 4], [5, 6]]
copied = original[:]

# 外側のリストは別オブジェクト
print("外側のリストは別?:", original is not copied)  # True

# しかし内側のリストは同じオブジェクト
print("内側のリスト[0]は同じ?:", original[0] is copied[0])  # True
print("内側のリスト[1]は同じ?:", original[1] is copied[1])  # True

図解:浅いコピーの構造

浅いコピーの場合:
original → [[1, 2], [3, 4], [5, 6]]
              ↑       ↑       ↑
copied   → [  同じ   同じ   同じ  ]  ← 外側は別だが内側は共有

深いコピー(ディープコピー)で完全にコピー

ネストされたリストを完全にコピーするには、「深いコピー」を使います。

copy.deepcopy()を使う

import copy

# 2次元リスト
original = [[1, 2], [3, 4], [5, 6]]
copied = copy.deepcopy(original)  # 深いコピー

print("コピー前:")
print("元のリスト:", original)  # [[1, 2], [3, 4], [5, 6]]
print("コピー:", copied)        # [[1, 2], [3, 4], [5, 6]]

# 内側のリストを変更
copied[0][0] = 100

print("変更後:")
print("元のリスト:", original)  # [[1, 2], [3, 4], [5, 6]] ← 変更されない!
print("コピー:", copied)        # [[100, 2], [3, 4], [5, 6]]

# 内側のリストも別オブジェクトになっている
print("内側のリスト[0]は別?:", original[0] is not copied[0])  # True

より複雑な例

import copy

# 3次元リスト
original = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]

# 浅いコピーでは不十分
shallow = original[:]
shallow[0][0][0] = 999
print("浅いコピー後の元のリスト:", original)  # [[[999, 2], [3, 4]], [[5, 6], [7, 8]]]

# 元に戻す
original = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]

# 深いコピーなら大丈夫
deep = copy.deepcopy(original)
deep[0][0][0] = 999
print("深いコピー後の元のリスト:", original)  # [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]

図解:深いコピーの構造

深いコピーの場合:
original → [[1, 2], [3, 4], [5, 6]]

copied   → [[1, 2], [3, 4], [5, 6]]  ← 全て別のオブジェクト

いろいろなデータ型でのコピー

辞書を含むリスト

import copy

original = [
    {"name": "太郎", "scores": [85, 90]},
    {"name": "花子", "scores": [88, 92]}
]

# 浅いコピーでは辞書の中身が共有される
shallow = original[:]
shallow[0]["scores"][0] = 100
print("浅いコピー後:", original[0]["scores"])  # [100, 90] ← 変更されてしまう

# 元に戻す
original[0]["scores"][0] = 85

# 深いコピーなら大丈夫
deep = copy.deepcopy(original)
deep[0]["scores"][0] = 100
print("深いコピー後:", original[0]["scores"])  # [85, 90] ← 変更されない

関数を含むリスト

import copy

def hello():
    return "Hello!"

def goodbye():
    return "Goodbye!"

original = [hello, goodbye, [1, 2, 3]]

# 関数はコピーされるが、参照は共有される
copied = copy.deepcopy(original)

print("関数は同じオブジェクト?:", original[0] is copied[0])  # True(関数は共有)
print("リストは別オブジェクト?:", original[2] is not copied[2])  # True(リストは別)

コピー方法の選び方

フローチャート形式で判断

def choose_copy_method(data):
    """適切なコピー方法を判断する"""
    
    # 1. リストの中身をチェック
    has_nested = any(isinstance(item, (list, dict)) for item in data)
    
    if not has_nested:
        print("推奨:浅いコピー ([:]、.copy()、list())")
        return data[:]
    else:
        print("推奨:深いコピー (copy.deepcopy())")
        import copy
        return copy.deepcopy(data)

# テスト
simple_list = [1, 2, 3, "hello"]
nested_list = [1, [2, 3], {"key": "value"}]

print("シンプルなリスト:")
choose_copy_method(simple_list)

print("\nネストされたリスト:")
choose_copy_method(nested_list)

一覧表

状況おすすめの方法理由
数値・文字列のみのリストlist[:] または .copy()シンプルで高速
リストの中にリストがあるcopy.deepcopy()内側も完全コピー
辞書を含むリストcopy.deepcopy()辞書の中身も完全コピー
大量のデータ必要に応じて判断deepcopyは処理が重い

パフォーマンスの比較

コピー方法によってパフォーマンスが異なります。

実際に測定してみる

import copy
import time

# テスト用データ
simple_data = list(range(10000))
nested_data = [[i, i+1] for i in range(1000)]

def measure_time(func, data, iterations=1000):
    """実行時間を測定する"""
    start = time.time()
    for _ in range(iterations):
        func(data)
    end = time.time()
    return (end - start) / iterations

# 各コピー方法の速度測定
print("シンプルなリストでの速度比較:")
print(f"[:]           : {measure_time(lambda x: x[:], simple_data):.6f}秒")
print(f".copy()       : {measure_time(lambda x: x.copy(), simple_data):.6f}秒")
print(f"list()        : {measure_time(lambda x: list(x), simple_data):.6f}秒")
print(f"copy.copy()   : {measure_time(lambda x: copy.copy(x), simple_data):.6f}秒")

print("\nネストされたリストでの速度比較:")
print(f"[:]           : {measure_time(lambda x: x[:], nested_data):.6f}秒")
print(f"copy.deepcopy(): {measure_time(lambda x: copy.deepcopy(x), nested_data):.6f}秒")

一般的な傾向

  1. 浅いコピー[:].copy() < list() < copy.copy()
  2. 深いコピーcopy.deepcopy()は浅いコピーより10倍以上遅い場合がある
  3. メモリ使用量:深いコピーは元のデータと同じ分だけメモリを使用

実践的な使用例

設定データの管理

import copy

# デフォルト設定
default_config = {
    "database": {"host": "localhost", "port": 5432},
    "cache": {"enabled": True, "ttl": 300},
    "features": ["login", "search", "upload"]
}

def create_user_config(user_overrides):
    """ユーザー固有の設定を作成"""
    # 深いコピーでデフォルト設定をコピー
    user_config = copy.deepcopy(default_config)
    
    # ユーザーの設定で上書き
    for key, value in user_overrides.items():
        if key in user_config:
            user_config[key].update(value)
    
    return user_config

# 使用例
user1_overrides = {"database": {"host": "db1.example.com"}}
user2_overrides = {"cache": {"ttl": 600}}

user1_config = create_user_config(user1_overrides)
user2_config = create_user_config(user2_overrides)

print("デフォルト設定:", default_config["database"]["host"])  # localhost
print("ユーザー1設定:", user1_config["database"]["host"])     # db1.example.com
print("ユーザー2設定:", user2_config["database"]["host"])     # localhost

ゲームの状態管理

import copy

class GameState:
    def __init__(self):
        self.board = [[0 for _ in range(3)] for _ in range(3)]
        self.current_player = 1
        self.move_count = 0
    
    def make_move(self, row, col):
        """手を打つ"""
        if self.board[row][col] == 0:
            self.board[row][col] = self.current_player
            self.current_player = 3 - self.current_player  # 1→2, 2→1
            self.move_count += 1
            return True
        return False
    
    def copy_state(self):
        """現在の状態のコピーを作成"""
        return copy.deepcopy(self)
    
    def display(self):
        """盤面を表示"""
        for row in self.board:
            print(row)
        print(f"現在のプレイヤー: {self.current_player}")
        print(f"手数: {self.move_count}")

# 使用例
game = GameState()
game.make_move(0, 0)  # プレイヤー1が(0,0)に
game.make_move(1, 1)  # プレイヤー2が(1,1)に

# 状態を保存
saved_state = game.copy_state()

# さらに手を進める
game.make_move(0, 1)
game.make_move(2, 2)

print("現在のゲーム状態:")
game.display()

print("\n保存された状態:")
saved_state.display()

データの変換処理

import copy

def safe_data_transformation(data, transform_func):
    """元データを保持しながらデータ変換を行う"""
    # 元データのバックアップを作成
    backup = copy.deepcopy(data)
    
    try:
        # データ変換を実行
        result = transform_func(data)
        return result, backup
    except Exception as e:
        print(f"変換エラー: {e}")
        return backup, backup  # エラー時は元データを返す

def risky_transformation(data):
    """リスクのあるデータ変換(例)"""
    for item in data:
        if "values" in item:
            # 危険な操作:0で割る可能性
            item["average"] = sum(item["values"]) / len(item["values"])
    return data

# 使用例
original_data = [
    {"name": "データ1", "values": [10, 20, 30]},
    {"name": "データ2", "values": []},  # 空リスト(エラーの原因)
    {"name": "データ3", "values": [5, 15, 25]}
]

result, backup = safe_data_transformation(original_data, risky_transformation)

print("元データは保護されています:")
print("バックアップの最初の項目:", backup[0])

よくある間違いと解決方法

間違い1:浅いコピーで十分だと思い込む

# 間違い
data = [{"users": ["Alice", "Bob"]}, {"users": ["Charlie"]}]
copied = data[:]
copied[0]["users"].append("David")
print(data[0]["users"])  # ['Alice', 'Bob', 'David'] ← 変更されてしまう

# 正しい
import copy
data = [{"users": ["Alice", "Bob"]}, {"users": ["Charlie"]}]
copied = copy.deepcopy(data)
copied[0]["users"].append("David")
print(data[0]["users"])  # ['Alice', 'Bob'] ← 変更されない

間違い2:copyモジュールのimportを忘れる

# 間違い
# data = copy.deepcopy(original)  # NameError

# 正しい
import copy
data = copy.deepcopy(original)

間違い3:無駄に深いコピーを使う

# 無駄(シンプルなデータに深いコピー)
simple_data = [1, 2, 3, "hello", True]
copied = copy.deepcopy(simple_data)  # 処理が重い

# 効率的
simple_data = [1, 2, 3, "hello", True]
copied = simple_data[:]  # 軽快

よくある質問

Q
どの方法が一番速い?
A

シンプルなリストなら[:].copy()が最速です。ネストされたデータなら必要性を考慮してcopy.deepcopy()を使いましょう。

Q
文字列や数値はコピーしなくても大丈夫?
A

文字列や数値はイミュータブル(変更不可)なので、コピーを考える必要はありません。

a = "hello"
b = a
a += " world"
print(b) # "hello" ← 変更されない
Q
大きなデータをコピーするとメモリ不足になる?
A

可能性があります。本当にコピーが必要か検討し、必要なら部分的なコピーやジェネレータの使用を検討しましょう。

Q
カスタムオブジェクトのコピーは?
A

copy.deepcopy()が使えますが、__copy__()__deepcopy__()メソッドを定義してカスタマイズできます。

import copy

class CustomObject:
def __init__(self, value):
self.value = value

def __deepcopy__(self, memo):
# カスタムコピー処理
return CustomObject(copy.deepcopy(self.value, memo))

まとめ

pythonの「リストのコピー」は、簡単そうでいて奥が深いテーマです。

おさらい:

  • 単純代入(=):コピーではなく参照の共有
  • 浅いコピー[:].copy()list()で1階層のみコピー
  • 深いコピーcopy.deepcopy()でネストされた構造も完全コピー
  • 使い分け:データ構造に応じて適切な方法を選択

コメント

タイトルとURLをコピーしました