Pythonをはじめてちょっとハマった部分。
似た記事がQiitaにも山ほどあるのですが・・・微妙にこの辺が書いてない感じがしたので。
Pythonはつい最近使いはじめたばかりのビギナーなので、どこかの点や、最後のまとめなどには、間違っている部分があるかもしれません。間違いがあったら、ぜひ指摘して教
えていただけると嬉しいです。
1/13 さっそくコメントで指摘をいただき、記事を大幅に修正しました。shiracamusさん、ありがとうございます。
前説
変数とオブジェクト、ミュータブルとイミュータブルについて
変数とオブジェクト
Pythonにおいて、変数とは、オブジェクトを指し示す識別子(名前)です。オブジェクトとは我々が扱いたいデータそのものです。
a = "abc"
上記において、変数名はaで、オブジェクトは**"abc"**という文字列です。"a"という変数名はあくまで"abc"というデータをプログラム内で便利に扱うための名前です。
変数名"a"は、"abc"というオブジェクトを指し示しているだけとも言えます。
a = "abc"
b = a
このようにした時、bもまた、aと同じオブジェクトを指します。同じオブジェクトを指していることを確認する方法として**id(変数名)**を使ってみます。
# コード
a = "abc"
b = a
print ("id(a) = %s" % id(a))
print ("id(b) = %s" % id(b))
# 実行結果
id(a) = 4339645944
id(b) = 4339645944
このように、二つの変数が、同じオブジェクトを指していることがわかります。
ミュータブルとイミュータブル
Pythonでは、オブジェクトを大別すると2種類あります。
- ミュータブルなオブジェクト(再代入可能なオブジェクト。大部分のオブジェクト)
- イミュータブルなオブジェクト(再代入が不可能なオブジェクト。文字列型や数字型、タプル型など)
この2種類のオブジェクトは、一度オブジェクトとして生成された後、別の値を再度代入する(値を更新する)際の動作が異なります。
例えば、
- ミュータブルなオブジェクトの場合
# ソースコード
a = [1,2,3]
print("a = %s" % a)
print("id(a) = %s" % id(a))
a.append(4)
print("a = %s" % a)
print("id(a) = %s" % id(a))
# 実行結果
a = [1, 2, 3]
id(a) = 4397053320
a = [1, 2, 3, 4]
id(a) = 4397053320
id(a)の結果に注目してください。
変数aというリストにあたらしい要素を追加した後も、同じオブジェクトIDを指しています。つまり、オブジェクトID"4397053320"は、値が更新されたということになります。
ミュータブルなオブジェクトでは、同じオブジェクト内のデータを更新することができます。
- イミュータブルなオブジェクトの場合
# ソースコード
a = "abc"
print("a = %s" % a)
print("id(a) = %s" % id(a))
a = a + "def"
print("a = %s" % a)
print("id(a) = %s" % id(a))
# 実行結果
a = abc
id(a) = 4432244216
a = abcdef
id(a) = 4434341816
先ほどとは異なり、変数aという文字列に新しい文字を連結した結果、変数aの指すオブジェクトのIDが変わりました。これは、文字列型はイミュータブルなオブジェクトであり、一度生成した後は値を変更できないためです。そのような場合は、別のオブジェクトが自動的に新規で生成され、変数名は、新しいオブジェクトを指すように、紐付けが変更されます。
ちなみに、このとき古いオブジェクト(上記の「id=4432244216」)がどうなるかというと、参照される変数が0個になったタイミングで破棄(解放)されます。1
複数の変数が同じオブジェクトIDを参照している場合
最初に紹介した**"b = a"** 代入のように、複数の変数が同じオブジェクトIDを参照している場合、ミュータブルとイミュータブルでは、次のように再代入の結果が異なります。
- ミュータブルなオブジェクトの場合
# ソースコード
a = [1,2,3]
b = a
a.append(4)
print ("a = %s" % a)
print ("b = %s" % b)
# 実行結果
a = [1, 2, 3, 4]
b = [1, 2, 3, 4]
aとaは同じオブジェクトを参照しており、かつオブジェクトに対する再代入(データの更新)が可能なため、bを生成した後でaのオブジェクトに変更を加えた場合に、bの中身も値が変わってしまいます。
一方、イミュータブルなオブジェクトでは、
- イミュータブルなオブジェクトの場合
# ソースコード
a = "abc"
b = a
a = a + "def"
print ("a = %s" % a)
print ("b = %s" % b)
# 実行結果
a = abcdef
b = abc
このように、変数aの値のみが変わります。これは、最初に'b=a'を行なったタイミングではaとbは同じオブジェクトIDを指していますが、イミュータブルなオブジェクトでは再代入時(上記の a = a +"def" の時)に、新しいオブジェクトが生成され、変数aは、新しいオブジェクト側を参照するように変数とオブジェクトの紐付けが変わるためです。
copyモジュールについて
ようやく本題に入ります。
ミュータブルなオブジェクトをコピーしたい、という場合、copyというモジュールがありまして。
# ソースコード
import copy
a = [1, 2]
b = copy.copy(a)
a.append(3)
print ("a = %s" % a)
print ("b = %s" % b)
print ("id(a) = %i" % id(a))
print ("id(b) = %i" % id(b))
# 実行結果
a = [1, 2, 3]
b = [1, 2]
id(a) = 140092728379976
id(b) = 140092728395208
このように、変数bはaとは異なるオブジェクトですが、その値はcopyメソッドを使ったタイミングで自動的にコピーされています。二つは異なるオブジェクトなので、コピー後に一方の値を変更しても、もう一方には影響しません。
#copyとdeepcopy
ところで、copyモジュールには、**copy()とdeepcopy()**の2つのメソッドがあります。
copy.copy(x)
x の浅い (shallow) コピーを返します。
copy.deepcopy(x)
x の深い (deep) コピーを返します。
浅い (shallow) コピーと深い (deep) コピーの違いが関係するのは、複合オブジェクト (リストやクラスインスタンスのような他のオブジェクトを含むオブジェクト) だけです:
浅いコピー (shallow copy) は新たな複合オブジェクトを作成し、その後 (可能な限り) 元のオブジェクト中に見つかったオブジェクトに対する 参照 を挿入します。
深いコピー (deep copy) は新たな複合オブジェクトを作成し、その後元のオブジェクト中に見つかったオブジェクトの コピー を挿入します。
リストや辞書を使った多層構造(多次元構造)の構造体や、オブジェクトをコピーする場合には、copy.copyだと「別のオブジェクトを作成したつもりが、その表面以外は全部同じデータを参照してた」ということがおこります。具体的なコードで説明してみます。
copyとdeppcopyの違い
copy(浅いコピー)の場合
先ほどのプログラムで、最初に生成するリストaを、いわゆる二次元配列にします。
# ソースコード
a = [[1, 2], [3, 4]]
b = copy.copy(a)
a[1].append(5)
print ("a = %s" % a)
print ("b = %s" % b)
print ("id(a) = %i" % id(a))
print ("id(b) = %i" % id(b))
# 実行結果
a = [[1, 2], [3, 4, 5]]
b = [[1, 2], [3, 4, 5]]
id(a) = 140092728721352
id(b) = 140092728316808
変数aと変数bのID自体は異なっているものの、変数bでも、配列の中身が更新されてしまっています。
これは、浅いコピーでは、新しいオブジェクト(この場合b)を作成するものの、その中身については、aと同じオブジェクトを参照しているからです。先ほどのコードの末尾に、二次元配列の内部配列”a[0], b[0], a[1], b[1]”についてもidを確認するprint文を追加してみます。
# ソースコード
import copy
a = [[1, 2], [3, 4]]
b = copy.copy(a)
a[1].append(5)
print ("a = %s" % a)
print ("b = %s" % b)
print ("id(a) = %i" % id(a))
print ("id(b) = %i" % id(b))
print ("id(a[0]) = %i" % id(a[0])) # 追加行
print ("id(b[0]) = %i" % id(b[0])) # 追加行
print ("id(a[1]) = %i" % id(a[1])) # 追加行
print ("id(b[1]) = %i" % id(b[1])) # 追加行
# 実行結果
a = [[1, 2], [3, 4, 5]]
b = [[1, 2], [3, 4, 5]]
id(a) = 140092866607048
id(b) = 140092739841480
id(a[0]) = 140092739841352
id(b[0]) = 140092739841352
id(a[1]) = 140092841262536
id(b[1]) = 140092841262536
この通り、変数の中身、2次元配列の内部配列については、同じIDを指していることがわかります。
これを防ぐには、deepcopyを使う必要があります。
deepcopy(深いコピー)の場合
上記と同じコードで、copy.copy()の部分をcopy.deepcopy()に変えてみます。
# ソースコード
import copy
a = [[1, 2], [3, 4]]
b = copy.deepcopy(a) #変更行
a[1].append(5)
print ("a = %s" % a)
print ("b = %s" % b)
print ("id(a) = %i" % id(a))
print ("id(b) = %i" % id(b))
print ("id(a[0]) = %i" % id(a[0]))
print ("id(b[0]) = %i" % id(b[0]))
print ("id(a[1]) = %i" % id(a[1]))
print ("id(b[1]) = %i" % id(b[1]))
# 実行結果
a = [[1, 2], [3, 4, 5]]
b = [[1, 2], [3, 4]]
id(a) = 140092840880264
id(b) = 140092728434760
id(a[0]) = 140092840879752
id(b[0]) = 140092728721352
id(a[1]) = 140092728396104
id(b[1]) = 140092867894216
このとおり。配列の中まで全てオブジェクトIDが異なっています。
まとめ
copyを使うケースや、copyを使わないで浅いコピーをする方法
基本的にはオブジェクトの深さに関わらずにcopy.deepcopyを使い、オブジェクトはコピーしたいが、中身は参照にしたいという目的があるケースにだけ、copy.copyを使った方が良い・・・のかなと思っていたのですが、コメントでshiracamusさんより「copyを使うケースは結構多いよ」と教えていただきました。
ソート処理は順番を入れ替えるだけなのでcopyで十分でしょう。
copyで十分なことは結構あります。リストはhoge[:]でコピーを作ることが多いです。
なるほど、浅いcopyで十分なケースも多々ありそうです。また、"import copy"せずとも、sliceを使えば同じ(浅いcopy)ことができるのですね。shiracamusさん、ありがとうございます。
最後に、スライスを使えば、浅いcopyができることの確認をしてみました。
# ソースコード
import copy
a = [[1],[2]]
b = a
c = a[:]
d = copy.copy(a)
print ('id(a) = %s, id(a[0]) = %s' % (id(a), id(a[0])))
print ('id(b) = %s, id(b[0]) = %s' % (id(b), id(b[0])))
print ('id(c) = %s, id(c[0]) = %s' % (id(c), id(c[0])))
print ('id(d) = %s, id(d[0]) = %s' % (id(d), id(d[0])))
# 実行結果
id(a) = 4466130888, id(a[0]) = 4466126984 <-コピー元の配列
id(b) = 4466130888, id(b[0]) = 4466126984 <単純な代入で作った変数
id(c) = 4466144264, id(c[0]) = 4466126984 <-[:](スライス)で作った変数
id(d) = 4466144648, id(d[0]) = 4466126984 <-copy.copyで作った変数
参照情報
-
https://docs.python.jp/3/reference/simple_stmts.html#assignment-statements
> 名前がすでに束縛済みの場合、再束縛 (rebind) がおこなわれます。再束縛によって、以前その名前に束縛されていたオブジェクトの参照カウント (reference count) がゼロになった場合、オブジェクトは解放 (deallocate) され、デストラクタ (destructor) が (存在すれば) 呼び出されます。 ↩