どうもこんにちは。うめきです。
これはアドベントカレンダーほぼ厚木の民の1日目の記事です。
本日は少しマニアックだけどとても重要なPythonのデータ同一性について書きます。こまごまと書いているので少しわかりにくいかもしれませんが、最後までお付き合いいただければ嬉しいです。
この記事を読んでわかること
- Pyhtonで書き換えていないリストまで値が書き換わってしまうのはなぜ?
- Pythonのデータの実体はどのように管理されているの?
- イミュータブルとミュータブルってなに?
- copyとdeepcopyの違いは?
おや?なんだかPythonの挙動がおかしい?
Pythonって簡単にかけるのが良いですよね。ただ、どうにでもかけちゃうので「実際この変数の実体はどこにあるんだろう?」とか、「なんでこんな挙動をするんだろう?」と思ったことはないでしょうか?例えばこんな例です。
def initialize_head(x):
x[0] = 0
return x
a = [1, 2, 3]
b = initialize_head(a)
print(f'a: {a}')
print(f'b: {b}')
処理としては非常にシンプルです。initialize_head
は入力リストの最初の値を0に変更して返す関数です。上記のコンソールへの出力結果を見てみましょう。
a: [0, 2, 3]
b: [0, 2, 3]
あれ?と思った方はいないでしょうか。b
の出力結果については問題ないと思いますが、a
の出力結果が[1, 2, 3]
であると思った人もいたのではないかと思います。
もうひとつ例を挙げます。
def to_list(data, default_list=[]):
try:
return list(data)
except:
return default_list
list1 = to_list(1)
list2 = to_list(2)
list1.append(1)
list2.append(2)
print(f'list1: {list1}')
print(f'list2: {list2}')
to_list
は入力データをリストに変換する関数です。リストに変換できない場合はデフォルト引数に指定されている空のリストを返します。list1
やlist2
は、これらの関数に1
や2
といった整数を渡しているのでリストに変換されずデフォルトの空リストが格納されているはずです。そして、これら2つのリストにそれぞれ1
と2
を追加しています。さて、出力はどのようになるでしょうか?
list1: [1, 2]
list2: [1, 2]
list1
は[1]
、list2
は[2]
という結果を予想していた方も多いのではないでしょうか。Pythonにおいて変数がどのように作成され、どのように管理されているかを知ればこれらの挙動の理由を知ることができます。この理由に加えて、上記の挙動を発生させないためにはどうすればよいのかを順番に解説していきます。
Pythonのデータはオブジェクト
Pythonにおいて、「データはすべてオブジェクトまたはオブジェクトの関係」で表現されます。この定義はPythonの公式サイトにも書かれています。例えば、1
は整数オブジェクト、0.1
は浮動小数点オブジェクト、'hoge'
は文字列オブジェクトといった感じです。そして、['apple', 'orange', 'banana']
はリストオブジェクトであり、リストオブジェクトの各要素は、'apple'
, 'orange'
, 'banana'
という3つの文字列オブジェクトで構成されています。つまり、すべてのデータは「オブジェクトまたはオブジェクトの関係」で表現できるということです。
さて、これらのオブジェクトたちはそれぞれ「型」・「値」・「同一性」を持ちます。このうち「型」と「値」は特に説明しなくてもよいと思います(1
について、「型」は整数型(int型)で、「値」は1
であるというだけ)。Pythonの挙動を正確に理解するために重要なものは「同一性」です。同一性(identity)は、生成されたあとには変更されないアドレスのようなものです。つまり、任意のオブジェクトが生成されてから削除されるまでそのidentityは変わることはありません。
オブジェクトのidentityは組み込みのid関数で取得することができます。
>>> a = 1
>>> id(a)
1858515168
変数a
に格納されている整数型オブジェクト1
のidentityが1858515168
であるということがわかりました。では、別の変数b
にこのa
を代入したらb
のidentityがどうなるか確認してみましょう。
>>> b = a
>>> id(b)
1858515168 # 変数aと同じidentity
変数b
についても、変数a
と同一の数値型オブジェクト1
を格納していることがわかりました。この事実から、Pythonにおける変数名は各オブジェクトにつけられるタグのようなものであると理解することができます。
つまり、変数が紐づいているオブジェクトが完全に同一のものであった場合、そのidentityは一致することになります。ん?ということは、a
をb
に代入しなくてもidentityは一致しているのでしょうか??早速確認してみましょう。今回は変数a
とb
をそれぞれ別で定義し、ソースコード上で紐づけられているようには書いていません。
>>> a = 1
>>> b = 1
>>> id(a)
1858515168
>>> id(b)
1858515168
a
とb
のidentityは一致しており、予想が的中しています。え、じゃあ同じ定義であればidentityはいつも一致しているのでしょうか?
>>> list1 = [1, 2]
>>> list2 = [1, 2]
>>> id(list1)
1994134265928
>>> id(list2)
1994134278664 # list1 と list2 のidentity は異なる
もちろん違います。新規に作成したリストは異なるオブジェクトとして管理されています。普通に考えて、別で定義したリストのアドレスが勝手に紐づいていたら、片方のリストの要素を変更しただけでもう一方のリストも書き換わってしまうので非常に不便です。ここで、最初の例では異なる変数が共通の実体を指示していたという事実がぼんやりと見えてきましたね。
ちなみに、整数型オブジェクト1はすでに生成されていましたので、list1
とlist2
で共通のオブジェクトに紐づいています。
>>> id(list1[0])
1858515168
>>> id(list2[0])
1858515168 # 整数1のidentityは共通
簡単なことを小難しく書いているかもしれませんがもう少し辛抱してください。
イミュータブルとミュータブル
ここまでで、Pythonのデータはオブジェクトで構成されており、変数はオブジェクトに紐づけられるタグのようなものであることが分かりました。では、オブジェクト本体との紐づけが変更されるケースと、オブジェクトとの紐づけが変わらないケースってどのようなときに発生するのでしょうか?ここを理解するためには、イミュータブル(変更不可)とミュータブル(変更可)について知っておく必要があります。
Pythonのオブジェクトは、イミュータブルなオブジェクトとミュータブルなオブジェクトに分けることができます。
- イミュータブル:同じオブジェクトのまま値を変更することができない
- ミュータブル:同じオブジェクトのままで値を変更することができる
例えば、int型やstr型はイミュータブルなオブジェクトです。したがって、以下のように変数a
の値を変更した場合、整数型オブジェクト1
の値を2
に変更することはできません。つまり、a
が1
であったときと、a
が2
であったときのid番号は異なることになります。別の言い方をするならば、a
に異なる値を代入した瞬間にa
は異なるオブジェクトに紐づけられることになります。
>>> a = 1
>>> id(a)
1858515168
>>> a = 2
>>> id(a)
1858515200
一方で、list型やdict型はミュータブルです。したがって、以下のように変数listA
の値を変更したとしても、listA
のid番号が変わることはありません。もちろんlistA
の最初の要素のid番号は整数型オブジェクト1
から別の整数型オブジェクト0
へと紐づけ(id番号)が変更されています。
>>> listA = [1, 2]
>>> id(listA)
1994134279048
>>> listA[0] = 0 # [0, 2]に変更
>>> id(listA)
1994134279048 # 値変更後もlistAは同一のオブジェクト
ここまでくると最初のよくわからない挙動を理解できるようになります。もう一度最初のコードを見てみましょう。
def initialize_head(x):
x[0] = 0
return x
a = [1, 2, 3]
b = initialize_head(a)
print(f'a: {a}') # a: [0, 2, 3]
print(f'b: {b}') # b: [0, 2, 3]
挙動としては以下の順番にデータが処理されていることになります。
-
a = [1, 2, 3]
の部分で新しいリスト型オブジェクトが作成される -
initialize_head
関数にa
が渡され、x = a
により関数内変数x
もa
と同一のリストオブジェクトに紐づけられる -
x
とa
が同一のリスト型オブジェクトに紐づけられた状態で、最初の要素が書き換えられる - 関数内変数
x
の結果が、b = x
により変数b
に渡されるため、変数b
も同一のリスト型オブジェクトに紐づけられる
ごちゃごちゃと書きましたが、最終的に変数a
とb
は同一のリスト型オブジェクトに紐づいていることになります。この結果から、イミュータブルなオブジェクトに関しては、式の代入だけでは別オブジェクトが生成されないために注意が必要ということがわかります。要するに、なんとなく代入して値書き換えとかやっていたら思わぬ書き換えが発生しちゃいますよということです。
最初にあげていたもう一つの例についても挙動を説明します。
def to_list(data, default_list=[]):
try:
return list(data)
except:
return default_list
list1 = to_list(1)
list2 = to_list(2)
list1.append(1)
list2.append(2)
print(f'list1: {list1}') # list1: [1, 2]
print(f'list2: {list2}') # list2: [1, 2]
こちらについては、Pythonの特徴的な挙動を知っていなければ完全な説明ができないためは少し意地悪でした(ごめんなさい)。その挙動というのは、デフォルト引数は一度しか評価されないというものです。つまり、default_list = []
が実行されるのは、関数が定義されるタイミングだけということになります。したがって、この関数で返されるdefault_list
というのは常に同一のオブジェクトに紐づいた状態になっています。list1
とlist2
は別々に定義されているように見えて、実は同一のリスト型オブジェクトに紐づけられていたのでした。
かなり余談ですが、デフォルト引数を動的に毎回評価したいときは、デフォルト引数をNone
にしておき、関数内に新しいオブジェクトを生成してターゲットの変数に渡す処理を記述する必要があります。
def to_list(data, default_list=None):
try:
return list(data)
except:
if default_list is None:
default_list = []
return default_list
いかがでしたか?Pythonの挙動を正しく知っておくことで、意図しない変数の書き換えは防ぐことができます。じゃあどう防ぐのか?ここを次に説明します。
copyとdeepcopy
値は同じだけど別のオブジェクトを生成したい!そんなときにはおなじみのcopy
モジュールを利用します。最初の例もcopy
モジュールを使えば解決します。
import copy
a = [1, 2, 3]
b = initialize_head(copy.copy(a))
print(a, b) # [1, 2, 3] [0, 1, 2]
なんだ簡単じゃないか!と思った方、こちらを見てください。
a = [[1, 2], [3, 4]]
b = copy.copy(a)
a[0][0] = 0
print(a) # [[0, 1], [3, 4]]
print(b) # [[0, 1], [3, 4]]
あれ、copy
モジュールじゃダメなの?もしかしてa
とb
が別のオブジェクトに紐づいているだけで、その中の値としてのリストオブジェクトは同一のオブジェクトを指示しているのかな?と気づくかと思います。実際に確認してみましょう。
>>> id(a) == id(b)
False
>>> id(a[0]) == id(b[0])
True
>>> id(a[1]) == id(b[1])
True
見事に予想が的中しております。関連するミュータブルなオブジェクト全てを再帰的に別のオブジェクトとして生成しないとこれまでの意図しない書き換えが発生してしまいます。この再帰的な別オブジェクトの生成を行ってくれるのがcopy.deepcopy
メソッドです。
>>> c = copy.deepcopy(a)
>>> id(a) == id(c)
False
>>> id(a[0]) == id(c[0])
False
>>> id(a[1]) == id(c[1])
False
確かに再帰的に内部のリストまで別オブジェクトとして生成されていることがわかります。copy
とdeepcopy
についてはもう少し細かい話も書けるのですが長くなってきたので今回は概要程度にとどめておきます。
まとめ
- Pythonのデータはオブジェクトとオブジェクトの関係で定義される
- Pythonにおける変数は各オブジェクトに紐づいたタグのようなもの
- オブジェクトはイミュータブル(変更不可)とミュータブル(変更可)が存在する
- ミュータブルなオブジェクトは思わぬ値の書き換えを発生させる可能性があるので必要に応じてcopyモジュールを使う
- copyモジュールにはcopyメソッドとdeepcopyが存在し、どこまでコピーするかに応じて使い分ける
おわりに
いかがでしたか?最初はよくわからなかったPythonの挙動も、少しだけ踏み込んで理解してみるだけでかなり見え方が変わったのではないでしょうか。今回は触れませんでしたが、numpyにもcopyとviewという概念があって、ある変数のviewを作成するといった場合には複数の変数が同一のオブジェクトを指し示すことになります。numpyのcopyメソッドは基本的にdeepcopyであるとかここも話し始めたら長いので気になる方はまた聞いてください。最後まで読んでいただきありがとうございました。