Pythonの代入とコピーの違いとは?|copyとdeepcopyを理解する

ソフトウェア

はじめに

Pythonでリストを別の変数に代入すると、「同じ内容のリストがもう1つ作られた」と思ってしまうことがあります。

たとえば、次のコードです。

x = [1, 2, 3]
y = x

y.append(4)

print(x)
print(y)

実行結果は、

[1, 2, 3, 4]
[1, 2, 3, 4]

となります。

y しか変更していないように見えるのに、なぜ x まで変わったのでしょうか。

理由は、y = x がリストをコピーする処理ではないからです。x と y は、同じリストオブジェクトを参照しています。

では、本当に別のリストを作りたい場合はどうすればよいのでしょうか。そこで登場するのが、copy() や copy.copy()、copy.deepcopy() です。

Pythonでは「代入」と「コピー」は別の操作です。
さらにコピーにも、「浅いコピー」と「深いコピー」という違いがあります。

この記事では、変数とオブジェクトの関係から、代入・浅いコピー・深いコピーの違いを順番に整理していきます。

この記事でわかること

この記事では、次の内容の理解を目指します。

  • Pythonの代入で何が起きているのか
  • なぜ y = x ではリストがコピーされないのか
  • copy() による浅いコピーとは何か
  • 浅いコピーで内部のリストが共有される理由
  • copy.deepcopy() による深いコピーとは何か
  • 代入・浅いコピー・深いコピーをどう使い分けるのか

1. まず「代入」と「コピー」を分けて考える

最初に、今回の記事でもっとも重要なポイントを確認しておきましょう。

Pythonでは、

y = x

と書いても、x が参照しているオブジェクトのコピーが自動的に作られるわけではありません。

前回の記事で説明したように、Pythonでは変数名とオブジェクトを分けて考えることが重要です。たとえば、

x = [1, 2, 3]
y = x

とすると、x と y という2つの変数名が、同じリストオブジェクトを参照します。つまり、y = x によって新しいリストが作られるわけではありません。

Pythonの代入ではxとyが同じリストオブジェクトを参照することを示した図

図のように、変数名は x と y の2つありますが、参照先のリストオブジェクトは1つです。そのため、y を通してリストの内容を変更すると、x から見ても変更後の内容が見えます。

代入はオブジェクトをコピーする操作ではありません。
y = x では、y も x と同じオブジェクトを参照するようになります。

この「代入ではオブジェクトが増えない」という点が、コピーを理解する出発点になります。

▶ Pythonの変数・オブジェクト・参照について詳しく知りたい
Pythonの変数は「箱」ではない?|オブジェクトと参照を理解する
Pythonの変数は本当に「値を入れる箱」なのでしょうか。オブジェクトと参照の仕組みを図解しながら、代入や再代入、ミュータブルとイミュータブル、コピー、参照カウントまでわかりやすく解説します。

2. 代入すると、なぜ片方の変更がもう片方にも見える?

では、冒頭で見たコードを、オブジェクトと参照の視点から考えてみましょう。

x と y は同じリストオブジェクトを参照しています。そのため、y.append(4) を実行すると、y だけが変化するのではなく、2つの変数名が参照しているその1つのリストオブジェクトの内容が変更されます。

つまり、「x まで変更された」のではありません。x と y の両方から、変更後の同じリストオブジェクトが見えているのです。

前回の記事で説明したように、list はミュータブルなので、同じオブジェクトのまま内容を変更できます。

「x まで変更された」のではなく、x と y が見ている同じリストオブジェクトが変更されたと考えるのがポイントです。

▶ ミュータブル・イミュータブルについて詳しく知りたい
Pythonのミュータブル・イミュータブルとは?|型ごとの違いを整理
Pythonのミュータブル・イミュータブルとは何かを初心者向けに解説。list、dict、set、int、str、tupleなどの型ごとの違いと、オブジェクトや参照、再代入との関係を図解しながらわかりやすく紹介します。

3. 別のリストを作るにはコピーする

では、x と同じ内容を持つ、別のリストオブジェクトを作りたい場合はどうすればよいのでしょうか。リストでは、copy() を使うことができます。

x = [1, 2, 3]
y = x.copy()

y.append(4)

print(x)
print(y)

結果は、

[1, 2, 3]
[1, 2, 3, 4]

となります。

今度は y に 4 を追加しても、x は変化していません。なぜなら、x.copy() によって、x が参照しているリストとは別のリストオブジェクトが作られ、それを y が参照しているからです。

概念的には、

x ─→ [1, 2, 3]

y ─→ [1, 2, 3]

となります。見た目の内容は同じでも、2つは別のリストオブジェクトです。

代入では同じオブジェクトを共有しますが、コピーすると別のオブジェクトが作られます。

ここまでを見ると、「では、リストは copy() しておけば完全に独立するのでは?」と思うかもしれません。ところが、リストの中にさらにリストが入っている場合は、少し事情が変わります。

4. copy() は「浅いコピー」

次のような入れ子のリストを考えてみましょう。

x = [[1, 2], [3, 4]]
y = x.copy()

copy() を使っているので、外側のリストについては x と y は別のオブジェクトです。しかし、次のコードを実行するとどうなるでしょうか。

y[0].append(5)

print(x)
print(y)

結果は、

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

となります。

「コピーしたはずなのに、また x まで変わった」と感じるかもしれません。ここで重要なのが、copy() は浅いコピー(shallow copy)だということです。

5. 浅いコピーでは何がコピーされる?

入れ子のリストでは、外側のリストが内側のリストそのものを直接持っている、と考えるより、内側のリストオブジェクトへの参照を持っていると考えると分かりやすくなります。

たとえば、

x = [[1, 2], [3, 4]]
y = x.copy()

とすると、新しい外側のリストが作られます。しかし、内側のリストまで新しく作られるわけではありません。新しく作られた外側のリストには、元のリストと同じ内側のオブジェクトへの参照が入ります。

Pythonの浅いコピーでは外側のリストは別になるが内側のリストは共有されることを示した図

図を見ると、x と y が参照する外側のリストは別ですが、その中から伸びる矢印は同じ内側のリストを指しています。つまり、浅いコピーでは「外側は別、内側の参照先は共有」という状態になります。

浅いコピーでは、外側のコンテナは新しく作られますが、その中にあるオブジェクトへの参照は共有されます。

そのため、y[0].append(5) とすると、x と y の両方から参照されている同じ内側のリストが変更されます。

だから、どちらから見ても内側のリストは [1, 2, 5] となるわけです。

6. 外側を変更した場合はどうなる?

浅いコピーをもう少し正確に理解するために、今度は外側のリストを変更してみましょう。

x = [[1, 2], [3, 4]]
y = x.copy()

y.append([5, 6])

print(x)
print(y)

結果は、

[[1, 2], [3, 4]]
[[1, 2], [3, 4], [5, 6]]

となります。

今度は y だけが変化しました。これは、x と y の外側のリストが別のオブジェクトだからです。

一方、y[0].append(5) のように内側のリストを変更すると、その内側のリストは共有されているため、x から見ても変更されています。

浅いコピーを理解するときは、「外側は別、内側の参照先は共有」と考えると分かりやすくなります。

7. copy.copy() でも浅いコピーを作れる

Pythonには、コピーを扱うための copy モジュールがあります。

import copy

x = [[1, 2], [3, 4]]
y = copy.copy(x)

copy.copy(x) も浅いコピーです。

リストの場合、y = x.copy() と y = copy.copy(x) は、どちらも外側のリストを新しく作り、内部のオブジェクトへの参照を共有する浅いコピーになります。

copy.copy() はリストだけでなく、さまざまなPythonオブジェクトに対して浅いコピーを作るための共通的な方法として使えます。

list.copy() と copy.copy() は書き方こそ違いますが、ここで扱っているリストでは、どちらも浅いコピーです。

8. deepcopy() は「深いコピー」

では、内側のリストまで別のオブジェクトにしたい場合はどうすればよいのでしょうか。そこで使うのが copy.deepcopy() です。

import copy

x = [[1, 2], [3, 4]]
y = copy.deepcopy(x)

y[0].append(5)

print(x)
print(y)

結果は、

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

となります。

今度は y の内側のリストを変更しても、x は変化しません。これは、deepcopy() が外側のリストだけでなく、その内部にあるオブジェクトについても必要に応じて再帰的にコピーしていくからです。

Pythonのdeepcopyでは外側だけでなく内側のリストも別のオブジェクトになることを示した図

先ほどの浅いコピーでは、外側のリストだけが別になり、内側のリストは共有されていました。一方、今回の例では、deepcopy() によって x 側と y 側で内側のリストも別のオブジェクトになります。

deepcopy() では、外側だけでなく、内部で参照しているオブジェクトについても再帰的にコピーしていきます。

そのため、入れ子になったミュータブルなデータを独立させたい場合に役立ちます。

9. 代入・浅いコピー・深いコピーを比較する

ここまでの違いを整理してみましょう。

方法 外側のオブジェクト 内側のオブジェクト
y = x 共有 共有
y = x.copy() 別 共有
y = copy.copy(x) 別 共有
y = copy.deepcopy(x) 別 必要に応じて再帰的にコピー

この違いを、言葉だけでなく「どのオブジェクトを共有しているのか」で考えることが重要です。

代入:同じオブジェクトを参照する
浅いコピー:外側だけ新しくし、内部の参照先は共有する
深いコピー:内部のオブジェクトも必要に応じて再帰的にコピーする

この3つを理解しておけば、Pythonのコピーで起こる多くの混乱を整理できます。

10. 「浅い」「深い」は入れ子の深さを考えるとわかりやすい

「浅いコピー」「深いコピー」という名前は、オブジェクトの参照関係を階層として考えると分かりやすくなります。

浅いコピーは、もっとも外側のコンテナをコピーします。一方、深いコピーは、その内部にあるオブジェクトについても参照関係をたどりながらコピーしていきます。

元のオブジェクト
↓
代入 → 同じオブジェクトを共有
↓
浅いコピー → 外側をコピー
↓
深いコピー → 内部も再帰的にコピー

ただし、「深いコピーなら常にすべてが完全に別物になる」と単純に考えるより、Pythonがオブジェクト間の参照関係をたどりながらコピーを作ると理解しておくほうが正確です。

11. どの方法を使えばいい?

では、代入・浅いコピー・深いコピーはどのように使い分ければよいのでしょうか。

同じオブジェクトを共有して使いたいのであれば、通常の代入 y = x で問題ありません。

一方、リストなどの外側のコンテナだけを別にしたい場合は、y = x.copy() のように浅いコピーを使います。

入れ子になったミュータブルなオブジェクトも含めて独立させたい場合は、copy.deepcopy(x) が候補になります。ただし、何でも deepcopy() すればよいわけではありません。

データが大きければコピーする対象も増えますし、そもそも内部のオブジェクトを共有して問題ない場合には、深いコピーは必要ありません。

コピーを選ぶときは、「どこまで別のオブジェクトにしたいのか?」を考えることが大切です。

12. コピーを理解すると「参照」がもっと見えてくる

ここまでの内容は、これまでの記事で説明してきた「変数」「オブジェクト」「参照」「ミュータブル」という考え方の延長にあります。

最初の記事では、Pythonの変数は単純な「値を入れる箱」と考えるだけでは説明できず、変数名とオブジェクトを分けて考えることを学びました。前回の記事では、オブジェクトには「同じオブジェクトのまま内容を変更できるもの」と「作成後に内容を変更できないもの」があることを確認しました。

そして今回、その考え方から、y = x がコピーにならない理由も説明できます。

変数名はオブジェクトを参照する
↓
ミュータブルなオブジェクトは内容を変更できる
↓
代入では同じオブジェクトを共有する
↓
別のオブジェクトが必要ならコピーする
↓
浅いコピーと深いコピーの違いを考える

代入とコピーの違いは、「値が同じか」ではなく、「どのオブジェクトを参照しているか」を考えると理解できます。

この視点は、次に扱う == と is の違いにもそのままつながっていきます。

まとめ

Pythonの代入とコピーについて、今回のポイントをまとめます。

  • y = x はオブジェクトをコピーする操作ではない
  • 代入すると、x と y は同じオブジェクトを参照する
  • list.copy() や copy.copy() は浅いコピーを作る
  • 浅いコピーでは外側のコンテナは別になるが、内部のオブジェクトへの参照は共有される
  • copy.deepcopy() は内部のオブジェクトについても再帰的にコピーしていく
  • コピーするときは「どこまで別のオブジェクトにしたいか」を考えることが重要

代入・浅いコピー・深いコピーの違いは、「どのオブジェクトが同じで、どのオブジェクトが別なのか」で考えると整理できます。

これまで、「変数名とオブジェクト」→「ミュータブルとイミュータブル」→「代入とコピー」と順番に見てきました。

次に理解したいのが、「値が同じこと」と「同じオブジェクトであること」の違いです。

次の記事では、Pythonの == と is が何を比較しているのかを、ここまで学んだオブジェクトと参照の考え方から整理していきます。

関連記事

前回の記事:Pythonのミュータブル・イミュータブルとは?|型ごとの違いを整理

Pythonのミュータブル・イミュータブルとは?|型ごとの違いを整理
Pythonのミュータブル・イミュータブルとは何かを初心者向けに解説。list、dict、set、int、str、tupleなどの型ごとの違いと、オブジェクトや参照、再代入との関係を図解しながらわかりやすく紹介します。

次回の記事:Pythonの「==」と「is」の違いとは?|値とオブジェクトの同一性を理解する

https://webstation-plus.com/python-copy/

関連書籍

Pythonの文法だけでなく、オブジェクトやデータモデルなど、Pythonの仕組みをもう少し深く理解したい人には『入門 Python 3』もおすすめです。

Amazon.co.jp
ソフトウェア
スポンサーリンク
toshi

京都市在住のエンジニア。読書好きが高じてブログを始め、現在は数学やAI・ソフトウェアを中心に、おすすめの小説についても記事を書いています。

toshiをフォローする
シェアする
toshiをフォローする

コメント

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