Python♪NumPyの「参照渡し」と「コピー」を覚えよう。リストとは全然違う。

 リストの「参照渡し」「浅いコピー」「深いコピー」とNumPyの配列の「参照渡し」「コピー」とは、コンピューターの内部でのメモリーの扱いも実行のしかたも全然違います。これを混同してしまうと大変なことになります。ここでは、NumPyの配列の「参照渡し」「コピー」について整理します。

 なお、リストの「参照渡し」「浅いコピー」「深いコピー」は、以下の記事で紹介しています。できれば、この記事を読む前に目を通してください。

参考記事:「参照渡し」「浅いコピー」「深いコピー」まずは、理屈抜きで覚えよう。

 また、どのようにコピーすればよいのか結果だけを知りたい方は、この記事の最後のまとめ(リストとNumPy配列のコピーの比較)だけをご覧ください。リストとNumPyの配列の「参照渡し」「浅いコピー」「深いコピー」「NumPyのコピー」等を網羅した比較表です。

1.リストとNumPyの配列はメモリ-の扱いが違う

 リストとNumPyの配列は、メモリーの扱いが異なるため、リストの考え方は通用しません。どのようにメモリーの扱いが違うかを簡単に(?)紹介したいと思います。
 リストではアドレスの参照の仕方を勉強した方がおもしろいと思いますが、NumPyの配列では、あまり、深みにはまらない方がよいのではないかと思います。

(1) リストの「参照渡し」のid

 コード01は、3行目で参照渡し(b=a)を行っています。参照渡しでは、aが参照しているリストのオブジェクト[1, 2]のアドレスをbに渡すため、aとbのidは同じになります。なお、このリストオブジェクトには要素である整数1、整数2のアドレスが記憶されています。aとbは同じリストオブジェクトを参照しているので、当然、a[0]とb[0]のidも同じになり、a[0]とb[0]は同じアドレスに記憶された整数1を参照します。

 次に、6行目でa[0]に3を代入した後のidを見てみましょう。a, bのidは代入前と同じです。つまり、同じリストオブジェクトを参照しています。一方、要素であるa[0], b[0]は代入前とは違うidを参照しています。

a[0] = 3では、要素の参照先を元の整数1とは別の場所に記憶した整数3に変更します。だから、代入前と代入後で、id(a[0])やid(b[0])は変化します。

しかし、aもbも同じリストオブジェクトを参照しているので、aが参照しているリストオブジェクトの内容が変更されれば、bの内容も連動して変更されます。だから、代入後のid(a[0])とid(b[0])は互いに同じidになります。

#コード01
a = [1, 2]
b = a #参照渡し
print('id(a), id(b) =', id(a), id(b)) #aとbのidは同じ
print('id(a[0]), id(b[0]) =', id(a[0]), id(b[0])) #aとbの要素のidも同じ。
a[0]=3 #要素の変更→aとbは連動する。
print('a, b =', a, b)
print('id(a), id(b) =', id(a), id(b)) #aとbのidは同じ
#以下、aとbの要素のidは同じですが要素の変更前とは変化している。
print('id(a[0]), id(b[0])=', id(a[0]), id(b[0])) 
#出力01
id(a), id(b) = 1998351361544 1998351361544
id(a[0]), id(b[0]) = 1346858464 1346858464
a, b = [3, 2] [3, 2]
id(a), id(b) = 1998351361544 1998351361544
id(a[0]), id(b[0]) = 1346858528 1346858528

(2) リストの「深いコピー」のid

コード02は、4行目で深いコピー(b=copy.deepcopy(a))を行います。深いコピーではリストオブジェクトを共有するのではなく、全く別の新しいリストオブジェクトを作成します。したがって、深いコピー後の、aとbのidは互いに異なります。

しかし、理解しにくいのは出力を見ると、深いコピー後のa[0]とb[0]が互いに同じidになっています。これは、わざわざ同じ整数要素を別の場所にも記憶させる必要はないというPython内部での節約術です。

同じアドレスの整数を共有すると、要素の変更が連動してしまうのではないかと思うかもしれませんが、要素が整数やタプルのような変更不能対(イミュータブル)の場合は問題にはなりません。

7行目でa[0]に3を代入するときに、もし、元の整数1が記憶されているアドレスに3を上書きすると変更が連動してしまいます。しかし、実際には代入する整数3は、整数1とは違うアドレスに記憶されます。整数1を書き換えるのではなく、整数1は放置し、全く新しい場所に整数3を再定義するのです。だから、a[0]に3を代入しても、b[0]は連動して変化しないのです。

7行目でa[0]に3を代入した後のidを見てみましょう。 b[0]のidは代入前と同じですが、3を代入したa[0]は代入前とidが異なります。

なお、 要素がリストのような変更可能体(ミュータブル)の場合には、idが同じだと深いコピーにならないので、値を共有せずidが変化します。 あくまで、それぞれの変数が互いに独立を保てる場合だけPythonが節約術を行使するのです。

この様に実際には内部で少し複雑な処理がなされている場合があります。しかし、考え方としては、深いコピーでは全く違うリストオブジェクトを作成すると考えておいた方が理解しやすいと思います。

つまり、重要なことは深いコピーでは変数a、bは互いに独立しており、片方の要素を変更しても他方が連動しないということです。

#コード02
import copy
a = [1, 2]
b = copy.deepcopy(a) #深いコピー
print('id(a), id(b) =', id(a), id(b)) #aとbのidは異なる
print('id(a[0]), id(b[0]) =', id(a[0]), id(b[0])) #a[0]とb[0]のidは同じ。
a[0] = 3 #要素の変更→aとbは連動しない。
print('a, b =', a, b)
print('id(a), id(b) =', id(a), id(b)) #aとbのidは異なる
print('id(a[0]), id(b[0]) =', id(a[0]), id(b[0])) #a[0]とb[0]のidは同じ。
#出力02
id(a), id(b) = 1998351639240 1998351361864
id(a[0]), id(b[0])= 1346858464 1346858464
a, b = [3, 2] [1, 2]
id(a), id(b) = 1998351639240 1998351361864
id(a[0]), id(b[0])= 1346858528 1346858464

2.NumPyの要素のidは変

NumPyのidを調べてみたのですが、要素のidについては、変な結果になることが分かりました。内容が趣味の世界になるので、記事をクリックで表示する形式にしました。興味のあるかただけ開いてください。ただ、ここで知っておかなければならないのは、リストとNumPyの配列は、アドレスの参照方法がまったく異なり、リストの時の考え方は通用しないと言うことです。

なお、NumPyの配列は、リストと比較し、行列計算が異常に速く科学技術計算にも耐えられる速度を実現できます。しかし、その速度を実現するためにリストにはない制約もあります。

①各要素のデータの型は同じでなければならない。
②多次元配列にする場合、各次元の要素数は同じでなくてはならない。
③メモリー上のデータの保存では連続したメモリー領域を確保している。
④原則、最初に決めた配列の大きさを途中で変えられない。x=numpy.append(x,y)で追加できなくもないが、メモリーを別の領域に確保しなおすため、リストのx.append(y)よりも遅い。

これらの制約や、内部での計算方法の様々な工夫によりNumPyの配列は、超スピードの行列計算が可能になっています。メモリーの参照の仕方が難しいのは仕方がないのかもしれません。

(1) NumPyの配列の「参照渡し」のid

記事の表示・非表示の切り替え

※ブラウザによっては最初から表示されてしまいます。(Google Chrome推奨)

コード03は、4行目で参照渡し(b = a)を行います。参照渡し直後は、aとbのidは互いに同じです。また、a[0]とb[0]のidも互いに同じになっています。ここまでは、リストと同じです。

次に、7行目でa[0]に3を代入した後のidを見てみましょう。a, bのidは互いに同じIDであり、a[0], b[0]のidもお互いに同じです。これも、リストと同じです。

しかし、リストの場合は代入前のa[0], b[0]のidと代入後のa[0], b[0]のidが同じだったのに対して、NumPyの配列では代入前後でidが変化しません。

この様に、リストとNumPy配列で、メモリーの参照の仕方が異なることが分かります。

#コード03
import numpy
a=numpy.array([1,2])
b=a #参照渡し
print('id(a), id(b) =', id(a), id(b)) #aとbのidは同じ
print('id(a[0]), id(b[0])=', id(a[0]), id(b[0])) #a[0]とb[0]のidは同じ。
a[0]=3
print('a, b =', a, b)
print('id(a), id(b) =', id(a), id(b)) #aとbのidは同じ
print('id(a[0]), id(b[0])=', id(a[0]), id(b[0])) #a[0]とb[0]のidは同じ。
#出力03
id(a), id(b) = 1998351755584 1998351755584
id(a[0]), id(b[0])= 1998351334328 1998351334328
a, b = [3 2] [3 2]
id(a), id(b) = 1998351755584 1998351755584
id(a[0]), id(b[0])= 1998351334328 1998351334328

ここまでは、変だとは思わなかったのですが・・・

(2) NumPyの配列の「コピー」のid

記事の表示・非表示の切り替え

※ブラウザによっては最初から表示されてしまいます。(Google Chrome推奨)

コード04は、4行目でNumPyで用意されたコピー(b=a.copy())を行います。コピー直後、aとbのidはそれぞれ異なります。しかし、a[0]とb[0]のidは互いに同じです。ここまではリストと同じです。次に、7行目でa[0]に3を代入した後のidを見てみましょう。a, bのidは代入前と同じです。しかし、リストと異なり、a[0], b[0]のidはどちらも代入前と変化しないのです。

これは、驚きです。a[0], b[0]のidはお互い同じなのに、a[0], b[0]はそれぞれ異なる値3, 1を示しているのです。全く、意味がわかりません。

単に私が勉強不足なだけなのですが、NumPyのアドレスの参照方法については、あまり深追いしない方がよいのではないかと思いました。(でも、どうなっているのか本当は知りたい)

#コード04
import numpy
a=numpy.array([1,2])
b=a.copy() #コピー(NumPyのメソッド)
print('id(a), id(b) =', id(a), id(b)) #aとbのidは異なる
print('id(a[0]), id(b[0])=', id(a[0]), id(b[0])) #a[0]とb[0]のidは同じ。
a[0]=3
print('a, b =', a, b)
print('id(a), id(b) =', id(a), id(b)) #aとbのidは異なる
print('id(a[0]), id(b[0])=', id(a[0]), id(b[0])) #a[0]とb[0]のidは同じ。
#出力04
id(a), id(b) = 1998351755664 1998351755904
id(a[0]), id(b[0])= 1998351334328 1998351334328
a, b = [3 2] [1 2]
id(a), id(b) = 1998351755664 1998351755904
id(a[0]), id(b[0])= 1998351334328 1998351334328

(3) NumPyの要素のidがもっと変になった

記事の表示・非表示の切り替え

※ブラウザによっては最初から表示されてしまいます。(Google Chrome推奨)

コード05はもっと変な結果になりました。全く別に定義したNumPy配列の要素も全て同じidになりました。NumPy配列の要素のidについては参照渡しとかコピーとか関係なかったみたいです。違う変数に代入したNumPy配列であっても、要素は同じオブジェクトとして一括管理しているということなのでしょうか?

やはり、私ごときがこれ以上の深追いは禁物でした。

#コード05
import numpy
a=numpy.array([1, 2])
b=numpy.array([5, 7, 1])
print('id(a), id(b) =', id(a), id(b))
print('変数aの要素のid=', id(a[0]), id(a[1]))
print('変数bの要素のid=', id(b[0]), id(b[1]), id(b[2]))
#出力05
id(a), id(b) = 2213156687104 2213156687424
変数aの要素のid= 2213156530000 2213156530000
変数bの要素のid= 2213156530000 2213156530000 2213156530000

3.NumPyの配列の「参照渡し」「コピー」のやり方を覚えよう。

 以下、NumPy配列の「参照渡し」と「コピー」のやり方を紹介します。理屈を知ろうとすると難しいですが、やり方は簡単です。すぐに覚えられます。なお、 NumPy配列 にはリストの「浅いコピー」に相当する者はなく、すべて「深いコピー」になります。

(1) NumPyの配列の「参照渡し」

 NumPyの配列の「参照渡し」は3通りあります。1つ目の方法は、5行目のように「b = a」と直接代入する方法です。2つ目の方法は10行目、15行目のようにスライスを使う方法です。3つめの方法は20行目のように、「import numpy, b=a.view()」とする方法です。「参照渡し」ですので、片方の変数の要素を変更すると、他方の変数の要素も連動して変更されます。

ここで注意しなければならないのは、リストでスライスを用いた場合は「浅いコピー」でしたが、NumPyでは「参照渡し」になることです。

また、 26行目 のように変数を再定義した場合は、それぞれ独立した変数となります。「変数の再定義」は、現在のデータを消して上書きするのではなく、現在の記憶場所を放置し、別の記憶場所に定義し直します。

#コード06
import numpy
    
a = numpy.array([1,2])
b = a #参照渡し
a[0] = 3
print(a, b) #連動する

a = numpy.array([1,2])
b = a[:] #参照渡し
a[0] = 3
print(a, b) #連動する

a = numpy.array([1,2])
b = a[:1] #参照渡し
a[0] = 3
print(a, b) #連動する

a = numpy.array([1,2])
b = a.view() #参照渡し
a[0] = 3
print(a, b) #連動する

a = numpy.array([1, 2])
b = a
a = numpy.array([3, 4]) #変数を再定義
print(a, b) #それぞれ独立
#出力06
[3 2] [3 2]
[3 2] [3 2]
[3 2] [3]
[3 2] [3 2]
[3 4] [1 2]

(2) NumPyの配列には「浅いコピー」はない

 NumPyの配列に「浅いコピー」はありません。リストの「深いコピー」に相当するコピーだけが存在し、それぞれの変数は完全に独立しています。コピーの書式は、b = a.copy()(書式1)とb=numpy.copy(a)(書式2)の2種類です。いずれもNumPyで用意されたメソッドであり、リストのcopy.copy()とは異なります。

#コード07
import numpy
a = numpy.array([1, 2])
b = a.copy() #書式1
a[0] = 3 #
print(a, b) #それぞれ独立

a = numpy.array([[1], [2]])
b = a.copy() #書式1
a[0][0] = 3 #深い部分の変更
print(a) #それぞれ独立
print(b)
a = numpy.array([1, 2])
b = numpy.copy(a) #書式2
a[0] = 3 #浅い部分の変更
print(a, b) #それぞれ独立

a = numpy.array([[1], [2]])
b = numpy.copy(a) #書式2
a[0][0] = 3 #深い部分の変更
print(a) #それぞれ独立
print(b)
#出力07
[3 2] [1 2]
[[3]
[2]]
[[1]
[2]]
[3 2] [1 2]
[[3]
[2]]
[[1]
[2]][3 2] [1 2]
[3 2] [1 2]

(3) NumPyの配列にリストのコピー方法を適用(実際は使わない)

 NumPyの配列でも、リストの浅いコピーで用いたb = copy.copy(a)や、深いコピーで用いたb = copy.deepcopy(a)を使用することはできます。しかし、b = copy.copy(), b = copy.deepcopy(a)は、それぞれ完全に独立した変数となりますので、あえて、リストの表記を使う必要はないと思います。

#コード08
import numpy
import copy

print('--------------')
a = numpy.array([1,2])
b = copy.copy(a)
a[0] = 3 #要素の浅い部分を変更
print(a, b) #それぞれ独立

print('--------------')
a = numpy.array([[1],[2]])
b = copy.copy(a)
a[0][0] = 3 #要素の深い部分を変更
print(a) #それぞれ独立
print(b)

print('--------------')
a = numpy.array([1, 2])
b = copy.deepcopy(a)
a[0] = 3 #要素の浅い部分を変更
print(a, b) #それぞれ独立

print('--------------')
a = numpy.array([[1],[2]])
b = copy.deepcopy(a)
a[0][0] = 3 #要素の深い部分を変更
print(a) #それぞれ独立
print(b)
#出力08
--------------
[3 2] [1 2]
--------------
[[3]
 [2]]
[[1]
 [2]]
--------------
[3 2] [1 2]
--------------
[[3]
 [2]]
[[1]
 [2]]

(4) NumPy:代入式の左辺にスライスを用いたコピー

  Pythonの配列では、コード09のように代入式の左辺にスライスを用いたコピー方法もあります。しかし、これは今までのように、定義されていない変数に代入する方法ではなく、すでにNumPyの配列を代入された変数に値をコピーする方法です。このとき、代入する側の配列の形と代入される側の配列の形は同じでなければなりません。
 代入される側(左辺)の配列の参照先は変わらないまま、右辺の要素をコピーするので、代入前に左辺の変数が別の変数と連動する状況であれば代入後も連動し、代入される前に完全に独立した変数であれば代入後も完全に独立した変数のままです。

#コード09
import numpy

a = numpy.array([1, 2])
b = a
b[:] = a #aがbを参照している状態なら、参照している状態のまま。
a[0] = 3
print('a, b =',a, b) #連動する

a = numpy.array([1, 2])
b = numpy.array([1, 2])
b[:] = a #aとbがそれぞれ独立した変数なら、独立した変数のまま。
a[0] = 3
print('a, b =',a, b) #それぞれ独立

print('--------')
a = numpy.array([[1], [2]])
b = a
b[:] = a #aがbを参照している状態なら、参照している状態のまま。
a[0][0] = 3
print(a) #連動する
print(b)

print('--------')
a = numpy.array([[1], [2]])
b = numpy.array([[1], [2]])
b[:] = a #aとbがそれぞれ独立した変数なら、独立した変数のまま。
a[0][0] = 3
print(a) #それぞれ独立
print(b)
#出力09
a, b = [3 2] [1 2]
a, b = [3 2] [3 2]
--------
[[3]
[2]]
[[3]
[2]]
--------
[[3]
[2]]
[[1]
[2]]

(5) リスト:代入式の左辺にスライスを用いたコピー

 リストの場合左辺にスライスを用いたコピーがどうなるか調べてみましょう。結果はコード10のようになりました。a[:]=bでも、a=b[:]と同じように浅いコピーができました。しかし、a=b[:]と違い、定義されていない変数に代入することはできません。また、NumPyの配列と異なり、配列の大きさが異なる場合でも代入することができます。

#コード10
print('--------')
a = [[1], [2]]
b = [[3], [4]]
print('代入前')
print(a)
print(b)
print('コピー前')
print(id(a), id(b))
print(id(a[0]), id(b[0]))
print(id(a[0][0]), id(b[0][0]))
b[:] = a #リストの一番上の参照を渡す→浅いコピーと同じ。
print('コピー後')
print(id(a), id(b))
print(id(a[0]), id(b[0]))
print(id(a[0][0]), id(b[0][0]))
print('a[0] = [5]')
a[0] = [5]
print(id(a), id(b))
print(id(a[0]), id(b[0]))
print(id(a[0][0]), id(b[0][0]))
print('代入後')
print(a) #連動しない
print(b)

print('--------')
a = [[1], [2]]
b = [[3], [4]]
print('代入前')
print(a)
print(b)
print('コピー前')
print(id(a), id(b))
print(id(a[0]), id(b[0]))
print(id(a[0][0]), id(b[0][0]))
b[:] = a #リストの一番上の参照を渡す→浅いコピーと同じ。
print('コピー後')
print(id(a), id(b))
print(id(a[0]), id(b[0]))
print(id(a[0][0]), id(b[0][0]))
print('a[0] = [5]')
a[0][0] = 5
print(id(a), id(b))
print(id(a[0]), id(b[0]))
print(id(a[0][0]), id(b[0][0]))
print('代入後')
print(a) #連動する
print(b)

print('--------')
a = [[1],[2]]
b = []
print('代入前')
print(a)
print(b)
#リストの配列の大きさが違っても代入できる。
#リストbが未定義の変数だとエラー
b[:] = a #一番上の参照を渡す→浅いコピーと同じ。
print('コピー後')
a[0][0] = 5
print('代入後')
print(a) #連動する
print(b)
#出力10
--------
代入前
[[1], [2]]
[[3], [4]]
コピー前
2409039712072 2409039806216
2409039642760 2409019096712
1346858464 1346858528
コピー後
2409039712072 2409039806216
2409039642760 2409039642760
1346858464 1346858464
a[0] = [5]
2409039712072 2409039806216
2409019096712 2409039642760
1346858592 1346858464
代入後
[[5], [2]]
[[1], [2]]
--------
代入前
[[1], [2]]
[[3], [4]]
コピー前
2409039796744 2409039391496
2409019095368 2409039712072
1346858464 1346858528
コピー後
2409039796744 2409039391496
2409019095368 2409019095368
1346858464 1346858464
a[0] = [5]
2409039796744 2409039391496
2409019095368 2409019095368
1346858592 1346858592
代入後
[[5], [2]]
[[5], [2]]
--------
代入前
[[1], [2]]
[]
コピー後
代入後
[[5], [2]]
[[5], [2]]

3.まとめ(リストとNumPy配列のコピーの比較)

  リストとNumPy配列の比較は下表となります。長々と解説しましたが、NumPy配列はリストの「浅いコピー」に相当するコピーがないため、リストよりは覚えやすいと思います。リストとNumPy配列はメモリーへの記憶方法などが全く異なるので、リストのコピー方法の考え方はまったく参考にならないことに注意していただければo.k.です。

記事topに戻る