【Python】変数に代入したら元の変数まで変わる!?原因と対策を分かりやすく解説

Pythonでリストをコピーしたつもりで別の変数に代入し、その変数を書き換えたら、なぜか元の変数まで変わっていた。Pythonを学び始めた人の多くが一度はぶつかる現象です。

a = [1, 2, 3]
b = a          # コピーしたつもり
b.append(4)    # b だけ変えたつもり

print(a)  # [1, 2, 3, 4]  ← a まで変わっている!
print(b)  # [1, 2, 3, 4]

先に結論を書きます。

  • 原因:b = a はコピーではなく、同じリストに2つ目の名前を付けただけだから
  • 対策:別物にしたいときは a.copy() でコピーする。リストの中にリストがあるときは copy.deepcopy(a) を使う

この記事では、なぜこうなるのかを図のイメージで説明し、よくあるハマりどころと対策を実際のコードで紹介します。

なぜ元の変数まで変わるのか?

変数は「箱」ではなく「名札」

プログラミングの入門書では、変数を「値を入れる箱」にたとえることがよくあります。この考え方だと、b = a は「aの箱の中身を、bの箱にコピーする」ように思えます。

しかしPythonの変数は、箱というより値(オブジェクト)に付ける名札です。

a = [1, 2, 3]   # [1, 2, 3] というリストを作り、「a」という名札を付ける
b = a           # 同じリストに「b」という名札も付ける

このとき、リストは1つしかありません。1つのリストに「a」と「b」の2枚の名札が付いている状態です。だから b を通してリストを書き換えると、同じリストを指している a から見ても変わっているのです。

思っていたイメージ 実際に起きていること
aの箱:[1, 2, 3]
bの箱:[1, 2, 3](コピー)
リスト [1, 2, 3] が1つだけあり、
そこに名札「a」と名札「b」が付いている
b = a のとき、変数は箱ではなく名札。1つのリストに名札aと名札bが付いている図
b = a は、1つのリストに2枚目の名札を付けるだけ

同じものかどうかは id() と is で確かめられる

2つの変数が同じオブジェクトを指しているかは、id() 関数や is 演算子で確認できます。

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

print(id(a), id(b))  # 同じ番号が2つ表示される(番号は実行ごとに変わる)
print(a is b)        # True(同じオブジェクト)

id() はオブジェクトごとに付いている識別番号を返します。番号が同じなら同じオブジェクトです。詳しくは「Pythonのid()とは?使い方・戻り値・オブジェクトの識別について解説」で解説しています。

数値や文字列では起きないのはなぜ?

「でも、数値なら代入しても元の変数は変わらなかったよ?」と思った人もいるでしょう。

x = 10
y = x
y += 5

print(x)  # 10  ← 変わらない
print(y)  # 15

これは、数値や文字列がイミュータブル(変更できない)なオブジェクトだからです。y += 5 は「10を15に書き換える」のではなく、15という新しい数値を作って、名札「y」をそちらに付け替える動きをします。10は書き換えられないので、名札「x」が付いている10はそのままです。

一方、リストや辞書はミュータブル(変更できる)なオブジェクトです。append() などはオブジェクトそのものを書き換えるので、同じオブジェクトを指すすべての名札に影響します。

種類 主な型 代入した先で書き換えると
ミュータブル(変更できる) list、dict、set、自作クラスのインスタンス 元の変数も変わる
イミュータブル(変更できない) int、float、str、tuple、bool 元の変数は変わらない(新しいオブジェクトができる)

ミュータブルとイミュータブルの違いは「【Python入門】ミュータブルとイミュータブルの違いをわかりやすく解説」で詳しく解説しています。

「代入し直す」と「中身を書き換える」は別物

リストでも、新しいリストを代入し直した場合は元の変数に影響しません。

a = [1, 2, 3]
b = a
b = [9, 9]     # 新しいリストに名札「b」を付け替えただけ

print(a)  # [1, 2, 3]  ← 変わらない
print(b)  # [9, 9]

元の変数まで変わるのは、append()、b[0] = 100、b["key"] = 値 のように、オブジェクトの中身を書き換えたときだけです。

対策1:copy() でコピーを作る

別物として扱いたいときは、代入ではなくコピーを作ります。

a = [1, 2, 3]
b = a.copy()   # 新しいリストを作る
b.append(4)

print(a)       # [1, 2, 3]  ← 変わらない
print(b)       # [1, 2, 3, 4]
print(a is b)  # False(別のオブジェクト)

リストのコピーには、次のような書き方もあります。どれも結果は同じです。

b = a.copy()   # copy()メソッド
b = a[:]       # スライス(全体を切り出す)
b = list(a)    # list()で作り直す

辞書も copy() でコピーできます。

p = {"name": "田中", "age": 20}
q = p.copy()
q["age"] = 21

print(p)  # {'name': '田中', 'age': 20}  ← 変わらない
print(q)  # {'name': '田中', 'age': 21}

copy() の使い方は「Python copy()とは?リストや辞書をコピーする方法を初心者向けに解説」で解説しています。

対策2:リストの中にリストがあるなら deepcopy()

ここで、もう一段階の落とし穴があります。リストの中にリストが入っている場合、copy() でも元の変数が変わることがあります。

a = [[1, 2], [3, 4]]
b = a.copy()
b[0].append(99)

print(a)  # [[1, 2, 99], [3, 4]]  ← また a まで変わった!
print(b)  # [[1, 2, 99], [3, 4]]

copy() がコピーするのは外側のリストだけで、中に入っているリストは同じものを指したままだからです。これを浅いコピー(シャローコピー)と呼びます。

print(a is b)        # False(外側のリストは別物)
print(a[0] is b[0])  # True(中のリストは同じもの)

中身まで丸ごとコピーしたいときは、copy モジュールの deepcopy() を使います。これを深いコピー(ディープコピー)と呼びます。

import copy

a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)
b[0].append(99)

print(a)  # [[1, 2], [3, 4]]  ← 変わらない
print(b)  # [[1, 2, 99], [3, 4]]
方法 コピーされる範囲 使いどころ
b = a コピーしない(同じものに名札を付けるだけ) 同じものを別の名前で扱いたいとき
a.copy() など 外側だけ(浅いコピー) 中身が数値や文字列だけのリスト・辞書
copy.deepcopy(a) 中身まですべて(深いコピー) リストの中のリスト、辞書の中のリストなど
b = a、a.copy()、copy.deepcopy(a) で、外側と中のリストがどこまで共有されるかを比べた図
どこまでが共有され、どこからが別物になるか

浅いコピーと深いコピーの違いは「Python copy()とdeepcopy()の違い|浅いコピー・深いコピーを解説」、deepcopy() の詳しい使い方は「Python deepcopy()とは?ネストしたリストを完全にコピーする方法」で解説しています。

よくあるハマりどころ4選

「代入したら元の変数まで変わる」現象は、b = a 以外の形でもよく起きます。どれも原因は同じで、1つのオブジェクトを複数の場所から指していることです。

1. [[0] * 3] * 3 で2次元リストを作る

grid = [[0] * 3] * 3
grid[0][0] = 1

print(grid)  # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]  ← 全部の行が変わった!
[[0] * 3] * 3 では3行が同じリストを指し、内包表記では行ごとに別のリストになることを示した図
どちらも grid[0][0] = 1 を実行した後の状態

* 3 は同じリストを3回並べるだけなので、3行とも同じリストを指しています。2次元リストは内包表記で作りましょう。

grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 1

print(grid)  # [[1, 0, 0], [0, 0, 0], [0, 0, 0]]

2. 関数に渡したリストが書き換わる

def add_item(items):
    items.append("new")
    return items

fruits = ["apple"]
result = add_item(fruits)

print(fruits)  # ['apple', 'new']  ← 呼び出し元のリストまで変わった!

関数の引数も「名札」なので、関数の中で書き換えると呼び出し元のリストも変わります。元のリストを残したいなら、関数の中でコピーしてから書き換えます。

def add_item(items):
    items = items.copy()   # コピーしてから書き換える
    items.append("new")
    return items

fruits = ["apple"]
result = add_item(fruits)

print(fruits)  # ['apple']  ← 変わらない
print(result)  # ['apple', 'new']

関数への渡し方の仕組みは「Pythonの参照渡しとは?値渡しとの違いと注意点を初心者向けに解説」で解説しています。

3. デフォルト引数に空のリストを使う

def add_log(msg, logs=[]):
    logs.append(msg)
    return logs

print(add_log("a"))  # ['a']
print(add_log("b"))  # ['a', 'b']  ← 前回の分が残っている!

デフォルト引数のリストは、関数を定義したときに1回だけ作られ、呼び出しのたびに同じリストが使い回されます。None を使って、呼び出しのたびに新しいリストを作るのが定番の書き方です。

def add_log(msg, logs=None):
    if logs is None:
        logs = []
    logs.append(msg)
    return logs

print(add_log("a"))  # ['a']
print(add_log("b"))  # ['b']

デフォルト引数の注意点は「Pythonデフォルト引数完全ガイド」でも解説しています。

4. ループの外で作ったリストを何度も追加する

rows = []
row = []
for i in range(3):
    row.append(i)
    rows.append(row)

print(rows)  # [[0, 1, 2], [0, 1, 2], [0, 1, 2]]  ← 全部同じになった!

rows には同じ row が3回追加されているだけなので、最後の状態が3つ並んで見えます。ループのたびに新しいリストを作りましょう。

rows = []
for i in range(3):
    row = [i]        # ループのたびに新しいリストを作る
    rows.append(row)

print(rows)  # [[0], [1], [2]]

pandasやNumPyでも同じことが起きる

データ分析でよく使う pandas や NumPy でも、似た現象が起きます。DataFrameやNumPy配列の一部を取り出すと、元のデータとメモリを共有したビューになることがあり、取り出した側を書き換えると元のデータまで変わる場合があります。

この場合も、.copy() で明示的にコピーを作るのが基本の対策です。詳しくは「Pandas DataFrameの「ビュー」と「コピー」を徹底解説」や「NumPy配列:ビュー?コピー?メモリ共有を正確に判定する方法」をご覧ください。

元の変数が変わってしまったときのチェックリスト

「なぜか元のデータが変わる」と思ったら、次の順に確認してみてください。

  1. 書き換えているのはリスト・辞書などのミュータブルなオブジェクトか?(数値や文字列なら、この現象は起きない)
  2. b = a のように、コピーせずに代入していないか? → a.copy() にする
  3. 中にリストや辞書が入っていないか? → copy.deepcopy(a) にする
  4. 関数の中で引数のリストを書き換えていないか? → 関数の中でコピーする
  5. デフォルト引数に [] や {} を使っていないか? → None にする
  6. 困ったら a is b や id() で、同じオブジェクトかどうかを確かめる

よくある質問(FAQ)

Q. なぜPythonは代入でコピーしないのですか?

大きなデータを代入のたびにコピーすると、時間もメモリも無駄になるからです。代入は名札を付けるだけにしておき、コピーが必要なときだけ明示的にコピーする、という設計になっています。

Q. copy() と deepcopy() はどちらを使えばいいですか?

リストや辞書の中身が数値や文字列だけなら copy() で十分です。中にリストや辞書が入っているなら deepcopy() を使います。迷ったら deepcopy() を使えば安全ですが、データが大きいと処理が遅くなる点に注意してください。

Q. タプルなら元の変数は変わりませんか?

タプル自体は書き換えられませんが、中にリストが入っている場合、そのリストは書き換えられます。たとえば t = ([1, 2], 3) の t[0].append(9) は実行でき、t は ([1, 2, 9], 3) になります。

Q. 文字列を += で書き換えても元の変数は変わりませんか?

変わりません。文字列はイミュータブルなので、+= は新しい文字列を作って名札を付け替えるだけです。

Pythonの基礎をしっかり身につけたい方へ

「代入」「コピー」「ミュータブル」のような仕組みは、エラーが出ないぶん独学では気づきにくいポイントです。変数の基本から確認したい方は「【Python入門】変数の使い方を初心者向けに完全解説!」、学習の進め方は「Python 始め方|初心者向け学習ロードマップと環境構築の手順」をご覧ください。

テックジムでは、未経験から参加できる無料のPython講座を毎週開いています。つまずきやすいポイントを、実際にコードを動かしながら確認できます。詳しくは「プログラミング無料体験・無料講座|テックジムの無料Python講座・Claude Code講座」をご覧ください。

プログラミング学習に興味がある方は、まずは無料体験会へ

無料体験会・お申し込みはこちら