python入門講座 | List型の使い方5(リストのコピー方法: 参照渡し、浅いコピー、深いコピー)[第27回]

アラサーOLのためのpython入門講座
3種類のコピー方法それぞれの挙動を理解して使い分けましょう。

Ciao!みなさんこんにちは!このブログでは主に
(1)pythonデータ解析,
(2)DTM音楽作成,
(3)お料理,
(4)博士転職
の4つのトピックについて発信しています。

今回は「アラサーOLのためのpython入門講座」です!この講座では、プログラミング初心者アラサーOLのMi坊さんに、pythonを学習する上でのアドバイスを行います!「パソコンもプログラミングも初心者だけど、プログラミングができるようになりたい!」という方のためにstep-by-stepで解説していきます。

今日はList型変数(リスト)をコピーする方法を解説します。あるリストを別の変数として複製する方法には(1)参照渡し、(2)浅いコピー、(3)深いコピーの3種類があります。それぞれ挙動が全く異なるので、理解して使い分ける必要があります。Pythonプログラミングでつまづきやすい点なので、しっかり覚えましょう!

この記事を読めば、List型変数をコピーする方法を知ることができます。ぜひ最後までお付き合いください!

Kaiko
Kaiko

この記事はこんな人におすすめ

  • 初心者だけどpythonを始めた!
  • pythonの基本的な使い方を知りたい!
  • 独学で学んだpythonの知識を整理したい!

Abstract | 想定通りのコードを書くためにコピー方法を使い分けよう

List型変数をコピーする方法には、

  1. 参照渡し
  2. 浅いコピー
  3. 深いコピー

の3種類があります。どの方法でも、コピー元のリストがコピー先の別の名前の変数にコピーされます。参照渡しでは、コピー先のリストに加えた変更がコピー元のリストにも反映されます。浅いコピーでは、コピー先のリストの0次元目の要素(浅い要素)の変更はコピー元には反映されない一方で、1次元目以上の要素(深い要素)を変更した場合はコピー元にも反映されます。深いコピーでは、コピー先のリストのどの要素の変更もコピー元には影響しません。

コピー先とコピー元で連動させたい場合は参照渡しを使う、連動させたくない場合には浅いコピーや深いコピーを使うなど、状況や行いたい処理によってコピー方法を使い分ける必要があります。深いコピーにすべきところで参照渡しを使った結果、コピー先のリストを編集することでコピー元のリストが上書きされてしまうなど、思わぬエラーが起こることもあります。3種類の方法を理解して使い分けましょう。



Background | Notebookの準備と配列でない変数のコピー

List型変数をコピーする方法を学び始める前に、Jupyter Notebookを立ち上げて準備をしましょう。また配列でない変数のコピーについておさらいしておきましょう。

準備 | python notebookの新規作成

まずはpython notebookを用意しましょう。いつものpython_practiceのディレクトリに「practice_list_copy」という名前のpython notebookを作成してください。ターミナルを立ち上げて~/python_practiceに移動、jupyter notebookを起動し、ブラウザから新規→Python3でpython notebookを開いて、ファイル→リネームでファイル名を決定します。
もしやり方がわからなければ、過去記事「python入門講座|pythonを使ってみよう2(Jupyter Notebookを使う方法)」などで詳しく解説しているので、これらを見ながらやってみてください。

python notebookを起動したら、適宜Markdownセルに説明書きを加えながら下記の説明に沿ってコードを書いて実行していきましょう。Markdownセルやコードセルなどの用語やこれらの使い方は過去記事「python入門講座|pythonを使ってみよう2(Jupyter Notebookを使う方法)」を参照してください。練習問題の後にpython notebookの例を掲載します。もし書き方がわからなければそちらを見てください。



変数のコピーのおさらい | 配列型でない変数のコピー

まずはおさらいとして、配列型でない変数、すなわちint, float, strなどの変数をコピーしたときの挙動を確認しましょう。以下を実行してください。

# define an int variable
var_orig = 3

# copy the variable
var_copy = var_orig

# change the copied variable
var_copy += 2

# print the two variables
print('original:', var_orig)
print('copied:', var_copy)

コピー元(original)は3、コピー先(copied)は5となります。このように、コピー先変数にコピー元変数を”=”で代入すれば、変数をコピーすることができます。さらに、コピー先の変数を変更してもコピー元の変数に影響はありません。

配列型でない変数のコピーとメモリアドレス

コピー先の変数を変更してもコピー元の変数に影響がない理由は、配列型でない変数の場合、コピー先変数とコピー元変数でメモリアドレスが異なっているためです。より正確には、「配列型ではない場合」ではなく、「変数がimmutable(変更不可能体)である場合」にこのような挙動になります。immutableとは部分的に変更できるという意味です。int型は部分的に変更することができないのでimmutableです。逆にList型はIndexingして変数の一部だけ変更できるのでmutable(変更可能体)です。
以下を実行して、配列型でない変数をコピーした際のメモリアドレスを確認しましょう。

# check memory address of the two variables
print('original:', id(var_orig))
print('copied:', id(var_copy))
print('id(original) == id(copied)?:', id(var_orig) == id(var_copy))

それぞれのメモリアドレスは異なっていることが確認できるはずです。

Method | リストのコピー方法

List型変数をコピーする方法を解説します。実際に手を動かしながら学んでいきましょう!Jupyter Notebookでコーディングしながら読み進めてください。練習問題の後にコーディング例も掲載しますので、やってみてどうしてもわからなければそれを見て進めてみてください。

List型変数のコピー1 — 参照渡し: コピー元とコピー先の全体が連動するコピー

ここからはリストのコピーの挙動を見ていきます。リスト以外にも、mutable(変更可能体)と呼ばれる変数の場合は同様の挙動となります。mutableとは「部分的に変更可能」な変数です。リストのように、indexingによって一部の要素だけを変更できものがmutableということです。他にはdict型(辞書型)などがmutableに当てはまります。

まずはコピー先変数の変更がコピー元に影響するタイプのコピー方法です。これは「参照渡し」と呼ばれる方法です。参照渡しでコピーしたコピー先変数の一部を変更すると、コピー元も変更されます。以下を実行してみましょう。

# define a list variable
var_orig = [3,4,5]

# copy the variable: call by reference
var_copy = var_orig

# change a part of the copied variable
var_copy[1] += 2

# print the two variables
print('original:', var_orig)
print('copied:', var_copy)

コピー先もコピー元も[3, 6, 5]というリストになります。途中でコピー先の1番目の要素のみ変更しましたが、コピー元にも同じ変更が適用されます。

参照渡しにおけるメモリアドレスの確認

参照渡しの場合、「var_orig」と「var_copy」のメモリアドレスは同一になります。メモリアドレスとはパソコンのメモリ上に確保された領域で、変数の「実体」のようなものです。参照渡しを行うと、実体は一つのまま、名前が2つ定義されるということです。以下を実行して確認しましょう。

# check memory address of the two variables
print('original:', id(var_orig))
print('copied:', id(var_copy))
print('id(original) == id(copied)?:', id(var_orig) == id(var_copy))

どちらも同じメモリアドレスであることが確認できるはずです。

変数全体の変更 = 初期化による連動解消

参照渡しでコピーしたコピー先変数の全体を変更すると、コピー元との連動は解消されます。変数全体の変更は「初期化」を意味し、新しい変数として定義されます。メモリアドレスも別々となるので連動が解消されます。以下を実行して確かめましょう。

# redefine the copied variable
var_copy = [1,2,3]

# print the two variables
print('original:', var_orig)
print('copied:', var_copy)

# check memory address of the two variables
print('original:', id(var_orig))
print('copied:', id(var_copy))
print('id(original) == id(copied)?:', id(var_orig) == id(var_copy))

コピー元とコピー先で、要素もメモリアドレスも異なることが確認できるはずです。コピー元とコピー先の連動は、コピー先変数の一部を変更をした場合に起こります。単独のint型変数のようにimmutableであれば、そもそも変数の一部を変更することができないので、コピーした変数を変更すると必ず新しいメモリアドレスが割り当てられます。そのためimmutableな変数ではコピー先とコピー元の連動を気にする必要はありません。

List型変数のコピー2 — 浅いコピー: コピー元とコピー先の0次元目が連動しないコピー

リストの0次元目の連動が切れるコピーを浅いコピー(shallow copy)と呼びます。1次元のリストの場合、浅いコピーを行えばコピー先の変更がコピー元に影響することはありません。「浅いコピー」というのはリストの「浅い部分」すなわち0次元目の連動が切れるコピーということです。

Slicingを使った浅いコピー

リストの浅いコピーではSlicingを使うのが簡単です。以下を実行してみましょう。

# define a list variable
var_orig = [3,4,5]

# copy the variable: shallow copy by slicing
var_copy = var_orig[:]

# change a part of the copied variable
var_copy[1] += 2

# print the two variables
print('original:', var_orig)
print('copied:', var_copy)

コピー元は[3, 4, 5]、コピー先は[3, 6, 5]となり、コピー元とコピー先の連動が切れていることがわかります。

浅いコピーのメモリアドレスの確認

浅いコピーの場合、「var_orig」と「var_copy」のメモリアドレス(実体)は別々になります。以下を実行して確認しましょう。

check memory address of the two variables
print('original:', id(var_orig))
print('copied:', id(var_copy))
print('id(original) == id(copied)?:', id(var_orig) == id(var_copy))

コピー元とコピー先でメモリアドレスが異なることが確認できます。

copyライブラリを使った浅いコピー: copy.copy()

浅いコピーではcopy.copy()を使うこともできます。こちらの方法はList型以外に、dict型(辞書型)などにも用いることができます。
以下を実行してみましょう。copyをimportしておく必要があります。通常import文は冒頭にまとめて書くべきですが、今回はここに書きます。

# import the library
import copy

# define a list variable
var_orig = [3,4,5]

# copy the variable: shallow copy
var_copy = copy.copy(var_orig) # same as var_orig[:]

# change a part of the copied variable
var_copy[1] += 2

# print the two variables
print('original:', var_orig)
print('copied:', var_copy)

コピー元は[3, 4, 5]、コピー先は[3, 6, 5]となります。コピー先の変更はコピー元に影響しません。

2次元的なリストに対する浅いコピー

浅いコピーで新しいオブジェクトとしてコピーされるのはリストの0次元目までです。リストの中にリストが格納されている2次元的なリストの場合、格納されているリストは参照渡しになります。すなわち、格納されているリストを部分的に変更する場合、コピー先の変更がコピー元に影響します。以下の例を実行して確認してみましょう。

# define a list variable
var_orig = [
    [1,2],
    [3,4]
]

# copy the variable: shallow copy by slicing
var_copy = var_orig[:]

# change a part of the copied variable
var_copy[0][1] += 2

# print the two variables
print('original:', var_orig)
print('copied:', var_copy)

実行結果はコピー元、コピー先ともに[[1, 4], [3, 4]]となります。コピー先の変更がコピー元に影響したことがわかりますね。

コピー先の要素の初期化 | コピー元との連動が切れる

リストに格納されたリストの一部を変更すると、コピー元も連動して変更されます。一方、リストに格納されたリスト全体を変更する場合、コピー元との連動が切れます。これは、前述の参照渡しでコピー後にリスト全体を更新したときと同様、格納されたリストが初期化されたことになるためです。参照渡しと浅いコピーにおける初期化と連動解除の関係は以下のようになっています。

  • リストの参照渡し→コピー先リスト全体の初期化でコピー元との連動が切れる
  • リストの浅いコピー→コピー先リストの要素の初期化でコピー元との連動が切れる

以下を実行して要素の初期化によってコピー元との連動が切れることを確認しましょう。

# define a list variable
var_orig = [
    [1,2],
    [3,4]
]

# copy the variable: shallow copy by slicing
var_copy = var_orig[:]

# change a part of the copied variable
var_copy[0] = [5,6]

# print the two variables
print('original:', var_orig)
print('copied:', var_copy)

var_copy[0] = [5, 6]という箇所で、コピー先の0番目の要素を初期化しています。ここで0番目の要素についてはコピー元との連動が切れています。実行結果はコピー元は[[1, 2], [3, 4]]のまま、コピー先は[[5, 6], [3, 4]]となります。

2次元的なリストに対する浅いコピーのメモリアドレスの確認

2次元的なリストに浅いコピーをかけたときのメモリアドレスを確認しておきましょう。浅いコピーなので、リスト全体のメモリアドレスはコピー元とコピー先で別々、各要素のメモリアドレスは同一となるはずですね。以下を実行して確認しましょう。

# define a list variable
var_orig = [
    [1,2],
    [3,4]
]

# copy the variable: shallow copy by slicing
var_copy = var_orig[:]

# check memory address of the two variables
print('--- Shallow component ---')
print('original:', id(var_orig))
print('copied:', id(var_copy))
print('id(original) == id(copied)?:', id(var_orig) == id(var_copy))

# check
print('--- Deep component ---')
print('original:', id(var_orig[0]))
print('copied:', id(var_copy[0]))
print('id(original) == id(copied)?:', id(var_orig[0]) == id(var_copy[0]))

「浅い成分(Shallow component)」であるリスト全体のメモリアドレス、id(var_orig)とid(var_copy)は別々となります。一方、「深い成分(Deep component)」であるリストの0番目要素のメモリアドレス、id(var_orig[0])とid(var_copy[0])は同一です。

var_orig[0]とvar_copy[0]はメモリアドレスが同一のリストです。参照渡しのところで前述したように、メモリアドレスが同一のmutableな変数(ここではリスト)の一部を変更すると、変更が両方の変数に反映されます。したがって、var_copy[0]の一部を変更すると、コピー元のvar_orig[0]にも反映されます。浅いコピーでは、格納された要素(この例では、var_copy[0]とvar_orig[0]など)が参照渡しでコピーされていると考えると理解しやすいでしょう。

List型変数のコピー3 — 深いコピー: コピー元とコピー先の全要素が連動しないコピー

次元に関わらず、リストのすべての要素の連動が切れるコピーを深いコピー(deep copy)と呼びます。深いコピーを行えばコピー先の変更がコピー元に影響することはありません。「深いコピー」とはリストの「深い部分」、すなわち高次元の要素の連動も切れるコピーを意味します。

copyライブラリを使った深いコピー: copy.deepcopy()

深いコピーにはcopy.deepcopy()を使います。Slicingを使うことはできないので、深いコピーのときには必ずcopy.deepcopy()を使います。以下を実行してみましょう。import文は冒頭等に書いてある前提でここでは省略します。

# define a list variable
var_orig = [
    [1,2],
    [3,4]
]

# copy the variable: deep copy
var_copy = copy.deepcopy(var_orig)

# change a part of the copied variable
var_copy[0][1] += 2

# print the two variables
print('original:', var_orig)
print('copied:', var_copy)

実行結果は、コピー元は[[1, 2], [3, 4]]のまま、コピー先は[[1, 4], [3, 4]]に変更されます。浅いコピーとは異なり、0番目要素であるリストの連動が切れていることがわかります。

深いコピーに対するメモリアドレスの確認

深いコピーにおけるリストのメモリアドレスを確認しておきましょう。以下を実行してください。

# define a list variable
var_orig = [
    [1,2],
    [3,4]
]

# copy the variable: deep copy
var_copy = copy.deepcopy(var_orig)

# check memory address of the two variables
print('--- Shallow component ---')
print('original:', id(var_orig))
print('copied:', id(var_copy))
print('id(original) == id(copied)?:', id(var_orig) == id(var_copy))

# check
print('--- Deep component ---')
print('original:', id(var_orig[0]))
print('copied:', id(var_copy[0]))
print('id(original) == id(copied)?:', id(var_orig[0]) == id(var_copy[0]))

浅い成分も深い成分もメモリアドレスが異なることが確認できます。メモリアドレスが異なっているので、コピー先の変更がコピー元に影響することはありません。

Result | 練習問題

最後に練習問題です。以下の処理を行うコードを書いてみてください。

  1. [1次元リストの参照渡し]
    変数var_origにMac, iPad, iPhoneの3つの文字列を代入し、List型変数として定義してください。
    変数var_copyにvar_origを参照渡しでコピーしてください。
    var_copyの0番目の要素をAppleに変更してください。
    var_origとvar_copyをprint()で出力してください。
  2. [1次元リストの浅いコピー]
    変数var_origにMac, iPad, iPhoneの3つの文字列を代入し、List型変数として定義してください。
    変数var_copyにvar_origを浅いコピーでコピーしてください。
    var_copyの0番目の要素をAppleに変更してください。
    var_origとvar_copyをprint()で出力してください。
  3. [2次元リストの浅いコピー]
    変数var_origを[[‘Mac’, 1249.5, 5], [‘iPad’, ‘329.00’, 12]]という要素を持つ2次元的なリストとして定義してください。
    変数var_copyにvar_origを浅いコピーでコピーしてください。
    var_copyのMacという要素をAppleに変更してください。
    var_origとvar_copyをprint()で出力してください。
  4. [2次元リストの深いコピー]
    変数var_origを[[‘Mac’, 1249.5, 5], [‘iPad’, ‘329.00’, 12]]という要素を持つ2次元的なリストとして定義してください。
    変数var_copyにvar_origを深いコピーでコピーしてください。
    var_copyのMacという要素をAppleに変更してください。
    var_origとvar_copyをprint()で出力してください。

練習問題の回答例

できたら回答例を確認しましょう!下の画面をスクロールすると回答例が見られます!参考にしてみてください。また、今回の記事で出てきた他のコードも載っているので参考にしてください!

練習問題おつかれさまでした!
今日はここまでです。Python Notebookを終了しておきましょう。もしPython Notebookを終了方法がわからなければ過去記事「python入門講座|pythonを使ってみよう2(Jupyter Notebookを使う方法)」の「Python Notebookの起動・終了方法」の章を参照してください!



Conclusion | まとめ

最後までご覧いただきありがとうございます!
今回はlist型変数をコピーする方法解説しました。参照渡し、浅いコピー、深いコピーの挙動を理解して使い分けましょう。Pythonのコードが思わぬ挙動をすることを防ぐことができ、思い通りのコードを書くことができるようになります!

以上「python入門講座 | List型の使い方5(リストのコピー方法: 参照渡し、浅いコピー、深いコピー)」でした!
またお会いしましょう!Ciao!



References | 参考

以下の教科書を参考にして進めています!より詳しく学びたい方は購入して読んでみてください!

Pythonの参考教科書

今回は以下のブログサイトも参考にしました。 mutable変数のコピーについて網羅的にまとめられています。

コメント

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