ねぇPythonのその仕様，本当に理解してる？？

[Python Advent Calendar 2022](https://qiita.com/advent-calendar/2022/python) 21日目の記事です．

## はじめに

みなさんPythonは好きですか？？Python，いいですよね．マジで手軽にかけるので自分も

- 簡単な殴り書きスクリプト
- 機械学習
- Webアプリ開発/WebAPI開発
- ChatBot開発
- 競プロ

などさまざまな用途に使ってきました．しかし，手軽すぎるゆえの落とし穴もあり，「Pythonよく書いているから完全に理解した」と思っていても思わぬところで勘違いしていることもあります．例えば，

```python
a = 1
b = 2
b = a
a = 3
print(a, b)
```

このPythonスクリプトを実行した結果はどうなりますか・・？？

```Python
a = [1, 2, 3]
b = a
a[0] = 100
print(a, b)
```

じゃあこれは・・・？？

あれ意外とわからんぞ，どっちだっけ．．てなりませんでしたか？？

ということで今回は，「以外と間違えそうなPython仕様クイズ」を何問か紹介したいと思います．ぜひ皆さんも全問正解目指して解いてみてください！！

解答はPython3.10で実行したものを記載しますが，Python3.9以上であれば同じ実行結果になります ~~（多分）~~ ．それより古い処理系を使っている人は自己責任でお願いします．例えば `list` の型アノテーションで引っかかるかと思います．ｺﾞﾒﾝﾅｻｲ

（特定のライブラリに関する問題は出題しません）

では早速1問目！！

## 1

### 問題

次のPythonスクリプトを実行したときの結果は？？（以後，"結果" は 「標準出力される文字列」と解釈してください）

```python
zero_list = [[0] * 3] * 3
#  zero_list = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
zero_list[0][0] = 1
print(zero_list)

```

【選択肢】

```text
1.  [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
2.  [[1, 0, 0], [0, 0, 0], [0, 0, 0]]
3.  [[1, 0, 0], [1, 0, 0], [1, 0, 0]]
4.  [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
```

### 答え

<details><summary>ここをめくる</summary>
正解は 3 の [[1, 0, 0], [1, 0, 0], [1, 0, 0]] でした！！
</details>

### 解説

競プロやってるときに予期せぬ動作になってハマったので問題にしてみました．

```python
zero_list = [[0] * 3] * 3
```

というコード， 

```python
zeros = [0, 0, 0]
zero_list = [zeros, zeros, zeros]
```

と等価だと考えればわかりやすいでしょうか． そうすると，

```python
zero_list[0][0] = 1
```

という操作は 

```python
zeros[0] = 1
```

という操作に言い換えられるので，結果として 

```python
print(zeros) 
#  >>> [1, 0, 0]

print(zero_list)
# = [zeros, zeros, zeros] なので
# >>> [[1, 0, 0], [1, 0, 0], [1, 0, 0]]
```

となります．

ちなみに，`zero_list[0][0] = 1` で本当に $(0,0)$ の要素だけを変更したい場合は， 

```python 
zero_list = [[0 for _ in range(3)] for _ in range(3)]
```

あるいは 

```python 
zero_list = [[0] * 3 for _ in range(3)]
```

などになります．この場合ならnumpyの [zeros](https://numpy.org/doc/stable/reference/generated/numpy.zeros.html) を用いてもいいですね．

## 2

### 問題

このスクリプトを実行した結果は？？

```Python
def add5(numbers: list[int]) -> list[int]:
    numbers[0] += 100
    return numbers


nums = [1, 2, 3]
nums2 = add5(nums)
print(nums, nums2)
```

【選択肢】

```text
1.  [1, 2, 3] [1, 2, 3]
2.  [1, 2, 3] [101, 2, 3]
3.  [101, 2, 3] [101, 2, 3]
4.  その他 / 一意に定まらない
```

### 答え

<details><summary>ここをめくる</summary>
正解は 3 の [101, 2, 3] [101, 2, 3] でした！！
</details>

### 解説


ミュータブル/イミュータブルと関数への引数の渡し方に関する問題でした．Pythonのリスト型や辞書型はミュータブル，一方で各種数値型や文字列型，タプルなどはイミュータブルです． 

で，ミュータブル，イミュータブルによって関数に引数として与えたときの挙動が若干異なります．ざっくりいうと，ミュータブルだと元の変数も（関数内から）変更されうる，一方でイミュータブルだと変更されない[^1]というものです．

[^1]: というかイミュータブルな変数というのが，IDを変えないと値を変えることができない（同一IDのまま書き換えができない）ので当たり前ではありますが・・・ 

今回の場合だと，Pythonのlist型はミュータブルなので，関数内での `numbers[0] += 1` によって元の `nums` まで書き換えられるという原理です．


## 3

### 問題

このスクリプトを実行したときにエラーは発生する？

```python
class Dog(object):
    def __init__(self, name):
        self.name = name 
        self._name = name  # 先頭にアンダーバー1つ
        self.__name = name  # 先頭にアンダーバー2つ
        self.__name__ = name  # 先頭と末尾にアンダーバー2つずつ


mydog = Dog("Fido")
print(mydog.name)
print(mydog._name)
print(mydog.__name)
```

【選択肢】

```text
1.  print(mydog.name) で発生する
2.  print(mydog._name) で発生する
3.  print(mydog.__name) で発生する
4.  print(mydog.__name__) で発生する
5.  発生しない
```

### 答え

<details><summary>ここをめくる</summary>
正解は 3 の print(mydog.__name) で発生する でした！！
</details>

### 解説

クラスとアンダーバーの扱いに関する問題でした．[pythonのアンダーバーこれなんやねん](https://qiita.com/kiii142/items/6879cb065ad4c5f0b901) にめちゃくちゃ良くまとまっているので，詳しい話はこちらを参照してください．

それぞれ簡単にみていくと，

```python 
self.name = name
```

単に，`name` 属性を初期化しています． 

```python 
self._name = name
```

`name` 属性を初期化していますが，先頭に `_` が一つついているので，外部からの参照を意図しないメッセージが込められます．（が実際に呼び出すことはできます）

また，関数名の先頭に `_` を一つつけておくと，ワイルドインポート（`from hoge import *`）の対象外となります． 

```python 
self.__name = name
```

マングリングが行われ，参照ができなくなります（参照しようとするとエラーが発生します）．参照したい場合は，

```python 
mydog = Dog("Fido")
print(mydog._Dog.__name)
```

とします．ということで正解は3でした．
おまけですが， 4の選択肢にある `__name__` はコードを実行する際にモジュール名が格納されるグローバル変数です（勝手に生成されます）．よく， 

```python 
if __name__ == '__main__':
    # なんらかの処理

```

の形式で見ることが多いと思います．これは，コマンドラインから直接実行されたときの処理を記述することができます．

単に名前の属性として `mydog.__name__` を用意するのはマジで紛らわしいのでやめましょう．

## 4

### 問題 

次のプログラムを **100回** 実行したときの実行結果はどうなる？？

```python
a = set([1, 5, 7])
#  a = {1, 5, 7}

for i in a:
    print(i, end="")
print()
```

【選択肢】

```text
1.  必ず毎回同じ実行結果になる
2.  毎回同じ実行結果にはならない
3.  不明/その他
```

### 答え　

<details><summary>ここをめくる</summary>
正解は 1 の 必ず毎回同じ実行結果になる でした！！
</details>

### 解説

Pythonの `set` （重複を許さない集合）に対するループの順序を問う問題でした． 

実は，Pythonの `set` は **全ての要素がint型** （あるいは特定の条件を満たしていれば）必ず同じ順番で繰り返しが行われます． 

実装を見てみましょう．Pythonの `set` は以下のコードで大元の実装を確認することができます．

https://github.com/python/cpython/blob/main/Objects/dictobject.c

コード内のコメントを一部抜粋すると，
> Major subtleties ahead:  Most hash schemes depend on having a "good" hash function, in the sense of simulating randomness.  Python doesn't:  its most important hash functions (for ints) are very regular in common
cases:
> \>>>[hash(i) for i in range(4)]
  [0, 1, 2, 3]
This isn't necessarily bad!  To the contrary, in a table of size 2**i, taking
the low-order i bits as the initial table index is extremely fast, and there
are no collisions at all for dicts indexed by a contiguous range of ints. So
this gives better-than-random behavior in common cases, and that's very desirable.

とあり，日本語訳すると， 
> ほとんどのハッシュでは，ランダムなシュミレーションを行うという意味で「良い」ハッシュ関数に依存しますが，Pythonはそうではありません． 
> （中略）
> サイズ 2 ** i の配列では，下位iビットを初期インデックスとすることで非常に高速に動作します．  

といった内容が書いてあります．（厳密にはもっとコードを追っていく必要があるのですが）少なくとも全ての要素がint型の場合には，上記に書いてあるようにある規則に従って格納されていくので，実行する度にループの順番が変わることはありません． 

他の言語だと，ループの順番にランダム性を持たせていることも多いので違和感を覚える方も多いと思います．例えばGo言語のmapなどはその例です（参考: [Iterating in maps](https://go.dev/doc/go1#iteration) ）

これまた面倒な挙動なのですが，`set` の中に文字列型の要素が入るとループの順番が実行する度に変わります．なんで・・・😭



## まとめ

いかがだったでしょうか．よく使っている言語でもよくわかっていない仕様などがあり，個人的にも新たな学びがたくさんありました．皆さんも使っている言語の面白い挙動，仕様などあればぜひコメントで教えてください！！

## 参考資料

- [python/cpython](https://github.com/python/cpython)
- [Pythonの引数における参照渡しと値渡しについて](https://www.javadrive.jp/python/userfunc/index3.html)
- [pythonのアンダーバーこれなんやねん](https://qiita.com/kiii142/items/6879cb065ad4c5f0b901)