Pythonのデータ同一性

どうもこんにちは。うめきです。
これはアドベントカレンダー[ほぼ厚木の民](https://adventar.org/calendars/5825)の1日目の記事です。
本日は少しマニアックだけどとても重要なPythonのデータ同一性について書きます。こまごまと書いているので少しわかりにくいかもしれませんが、最後までお付き合いいただければ嬉しいです。

## この記事を読んでわかること
- Pyhtonで書き換えていないリストまで値が書き換わってしまうのはなぜ？
- Pythonのデータの実体はどのように管理されているの？
- イミュータブルとミュータブルってなに？
- copyとdeepcopyの違いは？

## おや？なんだかPythonの挙動がおかしい？

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]`であると思った人もいたのではないかと思います。

もうひとつ例を挙げます。

```python:
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`といった整数を渡しているのでリストに変換されずデフォルトの空リストが格納されているはずです。そして、これら２つのリストにそれぞれ`1`と`2`を追加しています。さて、出力はどのようになるでしょうか？

```
list1: [1, 2]
list2: [1, 2]
```

`list1`は`[1]`、`list2`は`[2]`という結果を予想していた方も多いのではないでしょうか。Pythonにおいて変数がどのように作成され、どのように管理されているかを知ればこれらの挙動の理由を知ることができます。この理由に加えて、上記の挙動を発生させないためにはどうすればよいのかを順番に解説していきます。

## Pythonのデータはオブジェクト
<!--
- Pythonのデータはオブジェクト
- オブジェクトは「型」と「値」と「同一性」をもつ
- 同一性は生成されたあとは変更されない、オブジェクトのアドレスのようなもの（idの使い方）
- 変数名は各オブジェクトにつけられるタグのようなもの
- （図：箱に対してa, bがつけられている状態）
- １を別々に生成しても同じIDになること
- では、新しいオブジェクトが作られるときとそうでないときの違いは？
- -->

Pythonにおいて、「データはすべてオブジェクトまたはオブジェクトの関係」で表現されます。この定義は[Pythonの公式サイト](https://docs.python.org/ja/3/reference/datamodel.html#objects-values-and-types)にも書かれています。例えば、`1`は整数オブジェクト、`0.1`は浮動小数点オブジェクト、`'hoge'`は文字列オブジェクトといった感じです。そして、`['apple', 'orange', 'banana']`はリストオブジェクトであり、リストオブジェクトの各要素は、`'apple'`, `'orange'`, `'banana'`という３つの文字列オブジェクトで構成されています。つまり、すべてのデータは「オブジェクトまたはオブジェクトの関係」で表現できるということです。

さて、これらのオブジェクトたちはそれぞれ「型」・「値」・「同一性」を持ちます。このうち「型」と「値」は特に説明しなくてもよいと思います（`1`について、「型」は整数型（int型）で、「値」は`1`であるというだけ）。Pythonの挙動を正確に理解するために重要なものは「同一性」です。同一性（identity）は、生成されたあとには変更されないアドレスのようなものです。つまり、任意のオブジェクトが生成されてから削除されるまでそのidentityは変わることはありません。

オブジェクトのidentityは組み込みのid関数で取得することができます。

```python
>>> a = 1
>>> id(a)
1858515168
```

変数`a`に格納されている整数型オブジェクト`1`のidentityが`1858515168`であるということがわかりました。では、別の変数`b`にこの`a`を代入したら`b`のidentityがどうなるか確認してみましょう。

```python
>>> b = a
>>> id(b)
1858515168    # 変数aと同じidentity
```

変数`b`についても、変数`a`と同一の数値型オブジェクト`1`を格納していることがわかりました。この事実から、Pythonにおける変数名は各オブジェクトにつけられるタグのようなものであると理解することができます。

つまり、変数が紐づいているオブジェクトが完全に同一のものであった場合、そのidentityは一致することになります。ん？ということは、`a`を`b`に代入しなくてもidentityは一致しているのでしょうか？？早速確認してみましょう。今回は変数`a`と`b`をそれぞれ別で定義し、ソースコード上で紐づけられているようには書いていません。

```python

>>> a = 1
>>> b = 1
>>> id(a)
1858515168
>>> id(b) 
1858515168
```

`a`と`b`のidentityは一致しており、予想が的中しています。え、じゃあ同じ定義であればidentityはいつも一致しているのでしょうか？

```python

>>> list1 = [1, 2]
>>> list2 = [1, 2]
>>> id(list1)
1994134265928
>>> id(list2) 
1994134278664    # list1 と list2 のidentity は異なる
```

もちろん違います。新規に作成したリストは異なるオブジェクトとして管理されています。普通に考えて、別で定義したリストのアドレスが勝手に紐づいていたら、片方のリストの要素を変更しただけでもう一方のリストも書き換わってしまうので非常に不便です。ここで、最初の例では異なる変数が共通の実体を指示していたという事実がぼんやりと見えてきましたね。

ちなみに、整数型オブジェクト1はすでに生成されていましたので、`list1`と`list2`で共通のオブジェクトに紐づいています。

```python
>>> id(list1[0]) 
1858515168
>>> id(list2[0]) 
1858515168       # 整数1のidentityは共通
```

簡単なことを小難しく書いているかもしれませんがもう少し辛抱してください。

## イミュータブルとミュータブル
<!--
- イミュータブルとミュータブルの説明
- イミュータブルな変数は値が変更される（箱の中身が異なる値になる）と新しく変数がつくられること
- ミュータブルな変数は値が更新されても同一の実態を指し示すこと（箱の中のオブジェクトが変わるだけ）
- 最初の答え合わせ
- ではこれらの挙動を防ぐためにどうすればよい？
- -->

ここまでで、Pythonのデータはオブジェクトで構成されており、変数はオブジェクトに紐づけられるタグのようなものであることが分かりました。では、オブジェクト本体との紐づけが変更されるケースと、オブジェクトとの紐づけが変わらないケースってどのようなときに発生するのでしょうか？ここを理解するためには、イミュータブル（変更不可）とミュータブル（変更可）について知っておく必要があります。

Pythonのオブジェクトは、イミュータブルなオブジェクトとミュータブルなオブジェクトに分けることができます。

- **イミュータブル**：同じオブジェクトのまま値を変更することができない
- **ミュータブル**：同じオブジェクトのままで値を変更することができる

例えば、int型やstr型はイミュータブルなオブジェクトです。したがって、以下のように変数`a`の値を変更した場合、整数型オブジェクト`1`の値を`2`に変更することはできません。つまり、`a`が`1`であったときと、`a`が`2`であったときのid番号は異なることになります。別の言い方をするならば、`a`に異なる値を代入した瞬間に`a`は異なるオブジェクトに紐づけられることになります。

```python

>>> a = 1
>>> id(a) 
1858515168
>>> a = 2
>>> id(a) 
1858515200
```

一方で、list型やdict型はミュータブルです。したがって、以下のように変数`listA`の値を変更したとしても、`listA`のid番号が変わることはありません。もちろん`listA`の最初の要素のid番号は整数型オブジェクト`1`から別の整数型オブジェクト`0`へと紐づけ（id番号）が変更されています。

```python
>>> listA = [1, 2]
>>> id(listA) 
1994134279048
>>> listA[0] = 0    # [0, 2]に変更
>>> id(listA)    
1994134279048       # 値変更後もlistAは同一のオブジェクト
```

ここまでくると最初のよくわからない挙動を理解できるようになります。もう一度最初のコードを見てみましょう。

```Python:
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`は同一のリスト型オブジェクトに紐づいていることになります。この結果から、**イミュータブルなオブジェクトに関しては、式の代入だけでは別オブジェクトが生成されないために注意が必要**ということがわかります。要するに、なんとなく代入して値書き換えとかやっていたら思わぬ書き換えが発生しちゃいますよということです。

最初にあげていたもう一つの例についても挙動を説明します。

```python:
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`にしておき、関数内に新しいオブジェクトを生成してターゲットの変数に渡す処理を記述する必要があります。

```python
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で最初の問題が解決できることを確認
- でも2次元配列ではうまくいかないことをかく
- deepcopyを使えば完全に異なるIDで配列を生成できる
- list[:]でcopyもできるよ
- copyとdeepcopyの時間の違いは？
- copyですむときはcopyにしよう
-->

値は同じだけど別のオブジェクトを生成したい！そんなときにはおなじみの`copy`モジュールを利用します。最初の例も`copy`モジュールを使えば解決します。

```python
import copy

a = [1, 2, 3]
b = initialize_head(copy.copy(a))
print(a, b)    # [1, 2, 3] [0, 1, 2]
```

なんだ簡単じゃないか！と思った方、こちらを見てください。

```python
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`が別のオブジェクトに紐づいているだけで、その中の値としてのリストオブジェクトは同一のオブジェクトを指示しているのかな？と気づくかと思います。実際に確認してみましょう。

```python
>>> id(a) == id(b) 
False
>>> id(a[0]) == id(b[0]) 
True
>>> id(a[1]) == id(b[1]) 
True
```

見事に予想が的中しております。関連するミュータブルなオブジェクト全てを再帰的に別のオブジェクトとして生成しないとこれまでの意図しない書き換えが発生してしまいます。この再帰的な別オブジェクトの生成を行ってくれるのが`copy.deepcopy`メソッドです。

```python
>>> 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が存在し、どこまでコピーするかに応じて使い分ける

## おわりに
<!--
- numpyだったらcopyとviewという概念があるよ
- ndarrayだとnd[:]ではcopyじゃなくてviewになるんだ
- copyも浅くなる時と深くなる時あるから気を付けて（基本的に完全なndarrayならおっけい）
-->

いかがでしたか？最初はよくわからなかったPythonの挙動も、少しだけ踏み込んで理解してみるだけでかなり見え方が変わったのではないでしょうか。今回は触れませんでしたが、numpyにもcopyとviewという概念があって、ある変数のviewを作成するといった場合には複数の変数が同一のオブジェクトを指し示すことになります。numpyのcopyメソッドは基本的にdeepcopyであるとかここも話し始めたら長いので気になる方はまた聞いてください。最後まで読んでいただきありがとうございました。

## 参考文献
- [3. データモデル — Python 3.9.1rc1 ドキュメント](https://docs.python.org/ja/3/reference/datamodel.html#objects-values-and-types)
- [NumPyでのコピーあれこれ](http://labs.timedia.co.jp/2018/08/numpy-2.html)
- [[Python入門］Pythonのオブジェクトとは](https://www.atmarkit.co.jp/ait/articles/1907/12/news019.html)
- [[Python入門］オブジェクトの同一性、比較、文字列表現](https://www.atmarkit.co.jp/ait/articles/1907/16/news023.html)
- [Pythonの組み込みデータ型の分類表（ミュータブル等）](https://gammasoft.jp/blog/python-built-in-types/)
- [Pythonのcopyとdeepcopyについて](https://qiita.com/Kaz_K/items/a3d619b9e670e689b6db)
- [NumPyのコピー(copy)とビュー(view)を分かりやすく解説](https://deepage.net/features/numpy-copyview.html)