【Python】リストに追加した値が変わってしまう【参照の値渡し・ミュータブル】

# はじめに

プログラミング初学者です。Pythonで多次元リストを作成した際に学んだことを復習のためまとめました。はじめに私が書いたコードの誤りを訂正し、次になぜそれが起こったのかを説明します。解決に時間がかかってしまったので、同じ境遇の方が早く答えに辿り着く助けになればと思いつつ、詳しい説明はほかの方にお任せしてしまいました。初学者の限界をお許しください。

# 更新日
（2022/08/28）内容を大幅に変更しました。一番のミスは「<b>参照の値渡し</b>」を「<b>参照渡し</b>」だと誤認していたことです。正直、いただいたコメントが指南書として完成されているので、まずそちらをご覧いただければと思います。@shiracamusさま、@ttatsfさま、ご教示くださりありがとうございます。


# 環境

- Python 3.10.5

```bash
$ python --version
Python 3.10.5
```



# 発生した事象

```python
[[0], [0, 1], [0, 1, 2], [0, 1, 2, 3], [0, 1, 2, 3, 4]]
```

上のようなリストを作成したく、次のようなコードを書きました。

```python:main.py
result_list: list[list[int]] = []
num_list: list[int] = []

for num in range(0,5):
    num_list.append(num)
    result_list.append(num_list)

print(result_list)
```

しかし結果はこのようになってしまいました。

```bash
$ python main.py
[[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4]]
```



 # 解決方法

Pythonが関数に引数を渡す際、参照の値渡しという方法が用いられること、そしてリストがミュータブルなオブジェクトであることにより引き起こされたものでした。解決には、`.copy()`メソッドを用いるとよいそうです。

```diff_python:main.py
result_list: list[list[int]] = []
num_list: list[int] = []

for num in range(0,5):
    num_list.append(num)
-      result_list.append(num_list)
+      result_list.append(num_list.copy())  # num_listのコピーをappendする

print(result_list)
```

すると期待した結果が返ってきました！

```bash
$ python main.py
[[0], [0, 1], [0, 1, 2], [0, 1, 2, 3], [0, 1, 2, 3, 4]]
```


# 参照の値渡しとミュータブルについて
本稿のキーワードである「<b>参照の値渡し</b>」と「<b>ミュータブル</b>」についてご説明します。

```python
x: int = 1
```

このように変数を定義したとき、Pythonは二つのオブジェクトを生成します。ひとつは`x`というstr型の値を持つオブジェクト（仮にオブジェクトφと呼ぶ）、もうひとつは`1`というint型の値を持つオブジェクト（仮にオブジェクトψと呼ぶ）です。このとき、それぞれのオブジェクトには<b>メモリアドレス</b>という、いわゆるデータの（コンピュータ内での）住所も与えられます。ロッカーという例え[^1]も分かりやすいと思いました。そしてオブジェクトφはオブジェクトψを指すようになり、我々が`x + 1`とコーディングするだけで`1 + 1`を計算してくれます。

Pythonには、`id(x)`という組み込み関数があります。この関数の引数に先ほど定義した`x`（オブジェクトφ）を渡すと、戻り値としてオブジェクトψのメモリアドレスを取得できます。

そして、<b>参照の値渡し</b>とは、関数に引数として変数を指定した際、関数にアドレスを渡す方法のことです。我々が`x`と引数に入れると、オブジェクトψのアドレスが関数に渡されます。

<b>値渡し</b>という方法では、`1`そのものを渡します。ですがこの値が`1`ではなく、長い文字列や何個も要素があるリストである可能性を考えると、データの大きくないアドレスのほうが扱いやすいというメリットがあります。

また、（私が誤認した）<b>参照渡し</b>という方法もあります。メモリアドレスとは別の話です。
```python
def add(num):
    return num + 10:

x = 1
y = add(x)

print(y) // 11
print(x) // 1
```
Pythonは参照渡しできない言語なのでこのような結果になりますが、この`x`も`11`になる、つまり渡された変数そのものを変えてしまうのが参照渡しです。[^2]

そして、リスト型が<b>ミュータブル</b>であったためにこの問題に遭遇しました。ミュータブルとは「（変数定義後に）変更可」という意味で、「変更不可」なオブジェクトは<b>イミュータブル</b>といいます[^3]。

例えば、リスト型はミュータブルなので`lst = ['H', 'a', 'l', 'l', 'o']`に対して`lst[1] = 'e'`と一部を変更することができますが、イミュータブルな文字列型では`str = 'Hallo'`から一文字だけ変更することはできません。

```python:main.py
result_list: list[list[int]] = []
num_list: list[int] = []

for num in range(0,5):
    num_list.append(num)
      result_list.append(num_list)

print(result_list)
```
もとのコードでは`for`文で5回`num_list`を引数に指定していますが、そのすべてが同じアドレスを渡していたため、変更後のオブジェクトを5回追加するという結果になってしまったようでした。

イミュータブルなオブジェクトの場合、値を変えようと`str = 'Hello'`、`x = 10`、`x += 1`などと再代入すると思いますが、このとき新しくオブジェクトが生成され、`str`や`x`は新しいほうのオブジェクトを指すようになります。このときアドレスも変わるので、同様の事象は起こらず、今まで勉強しなくても済んでいたようでした。


# 参考文献

[^1]:[メモリとアドレス](http://www.ci.seikei.ac.jp/CompFund/MemoryAndAddress.pdf)
[^2]:[参照渡し](https://wa3.i-3-i.info/word16070.html)
[^3]: [ミュータブルな型とイミュータブルな型の相違を知ろう](https://qiita.com/makotoo2/items/fc3a617882916f9775f5)