『Effective Python 第2版』 第2章＜リストと辞書＞

# はじめに
* こんにちは、takadowaです。
* 前回の[『Effective Python 第2版』 第1章＜Pythonic思考＞](https://qiita.com/takadowa/items/44db07e227a58287b193)に引き続き、今回は第2章＜リストと辞書＞の内容をベースに、さらに調べて知ったことや自分の感想をまとめていきたいと思います。

# 実行環境
サンプルコードを掲載する際は、以下の実行環境で実行した結果を掲載します。
できる限りミスのない形で提供することを心がけますが、動かない、間違っているなどがありましてもご容赦ください。

* macOS 10.14.6
* Python 3.8.5

# 第2章＜リストと辞書＞
## catch-allアンパック

通常のアンパックは `a, b = [1, 2]` のように書きますが、この書き方だと値の数が一致しない場合、エラーが発生します。

```python
# 通常のアンパックだと、左辺と右辺とで値の数が一致しない場合はエラーとなる
a, b = [1, 2, 3, 4]
>>>
...
ValueError: too many values to unpack (expected 2)
```

「いくつかのサンプルデータを見たところ2つだと思っていたが、実は3つ以上の値が入ることもあった」なんてことも開発しているとあるのではないでしょうか？愚直に実装すると以下のようになりますが、これは行数も多くなるし、バグにもなりやすいので良くありません。

```python
# スライスやインデックスを使ったイケてない例
l = [1, 2, 3, 4]
a = l[0]
b = l[1]
c = l[2:]
```

そのようなときに便利なのが「catch-allアンパック」です。

```python
# catch-allアンパックを使う
a, b, *c = [1, 2, 3, 4]
print(a, b, c)
>>>
1 2 [3, 4]  # cに残りのすべてがリストで入っている
```

上記例のように、先頭に`*`をつけた変数が残りのすべての値をリストで保持してくれるので、さきほどのような値の数が一致しないことによるエラーはなくなります。

また、以下のように、仮に要素が残っていなかったとしても、空リストになるので安心です。

```python
# cに入る要素は残っていないがエラーにはならず、空リストが入る
a, b, *c = [1, 2]
print(a, b, c)
>>>
1 2 []  # cは空リストになる
```

このcatch-allアンパックは、最後だけでなく、先頭や真ん中に指定することも可能なので、状況に応じて使い分けることが可能です。

```python
# 先頭でcatch-allアンパック
*a, b, c = [1, 2, 3, 4]
print(a, b, c)
>>>
[1, 2] 3 4

# 真ん中でcatch-allアンパック
a, *b, c = [1, 2, 3, 4]
print(a, b, c)
>>>
1 [2, 3] 4
```

ちなみに、これらの詳しい説明は[PEP 3132 -- Extended Iterable Unpacking](https://www.python.org/dev/peps/pep-3132/)に書かれているようです。`*a = range(5)`はエラーになるが、`*a, = range(5)`はOKな書き方だそうで、へぇ〜ってなりました。

## Pythonのソートは安定ソートである

[ソートの安定性と複合的なソート -- Python公式ドキュメント](https://docs.python.org/ja/3/howto/sorting.html#sort-stability-and-complex-sorts) に

> ソートは、 安定 (stable) であることが保証されています。これはレコードの中に同じキーがある場合、元々の順序が維持されるということを意味します。

と書かれているとおり、Pythonのソートは安定ソートだそうです。また、具体的に利用されているアルゴリズムも以下のように記されています。

> Python では Timsort アルゴリズムが利用されていて、効率良く複数のソートを行うことができます、これは現在のデータセット中のあらゆる順序をそのまま利用できるからです。

ちなみに、[Timsortを英語版Wikipedia](https://en.wikipedia.org/wiki/Timsort)で調べると、実装したのはTim Petersという方だそうで、Tim Petersは[前回の記事](https://qiita.com/takadowa/items/44db07e227a58287b193)でも紹介した「The Zen of Python」を書いた人でもあります。私はこの方を今回初めて知りましたが、Pythonに詳しい方ならみんな知っているのでしょうか？？:thinking:

## dictの挿入順序について

> Python3.5以前では、dictをイテレートすると任意の順序でキーが返されるようになっていました。
> （中略）
> Python3.6、Python3.7以降はPythonの仕様として、辞書は挿入順を保持するようになりました。

と、書籍に書かれていたので、実際に調べてみました。

```python
# 下記のコードをPython3.5〜3.8でそれぞれ実行してみました
d = {"a": 1, "b": 2, "c": 3}
print(d)

# Python 3.5.9
>>>
{'b': 2, 'a': 1, 'c': 3}  # 順序バラバラ

# Python 3.6.11
>>>
{'a': 1, 'b': 2, 'c': 3}  # 順序どおり！

# Python 3.7.8
>>>
{'a': 1, 'b': 2, 'c': 3}  # 順序どおり！

# Python 3.8.5
>>>
{'a': 1, 'b': 2, 'c': 3}  # 順序どおり！
```

たしかに、Python3.6以降では挿入した順序どおりに表示されていますね！

ちなみに、組み込みモジュール`collections`に`OrderedDict`という挿入順序を保証するクラスがありますよね。Python3.6以降では「もうこれ不要じゃない？」と思われるかもしれません（思いました）が、実はそうじゃないそうです。

書籍によると

> 振る舞いは標準`dict`型と同じだが、`OrderedDict`の性能特性はまったく異なる。キーの挿入と`popitem`呼び出しが高頻度なら（例えばLRUキャッシュの実装）標準`dict`型よりも`OrderedDict`のほうが良い

とのことです。ここまで意識して使い分けるスキルは自分にはありませんが、頭の片隅に置いておきたいなーと思いました。

## 特殊メソッド`__missing__`で欠損キーの扱いを実装できる

dictを使っているとき、存在しないキーにアクセスしたらKeyErrorが発生するのはよくご存知のことだと思います。

```python
# 存在しないキーにアクセスしたらKeyErrorが出る
d = {"a": 1}
print(d["b"])
>>>
...
KeyError: 'b'
```

この存在しないキーにアクセスしたときの振る舞いを実装できるのが特殊メソッド`__missing__`で、以下のようにdictのサブクラスとして定義します。

```python
# __missing__を使って存在しないキーにアクセスされたときの振る舞いを実装する例
class MyDict(dict):
    def __missing__(self, key):
        # 存在しないキーにアクセスしたらそのキーをそのまま返す
        return key

my_dict = MyDict()
my_dict["a"] = 1
print(my_dict["a"], my_dict["b"])
>>>
1 b  # キーが存在する場合はバリューが返り、存在しない場合はキーが返っていることがわかる
```

# まとめ
『Effective Python 第2版』 第2章＜リストと辞書＞ の内容をベースに、調べて知ったことや感想を記しました。

* catch-allアンパックを使えば、スライスやインデックスを使うよりシンプルかつバグのリスク低く実装できるでしょう
* Pythonのソートは安定ソートで、アルゴリズムにはTimsortが使われています
* Python3.6以降ではdictの挿入順序が保証されるようになりました
    - dictとOrderedDictは振る舞いは同じですが、性能特性はまったく異なるそうです
* 特殊メソッド`__missing__`を使えば、dictの存在しないキーにアクセスしたときの振る舞いを実装することができます

# 参考文献
* Effective Python 第2版
    - https://www.oreilly.co.jp/books/9784873119175/

# 参考サイト
* PEP 3132 -- Extended Iterable Unpacking
    - https://www.python.org/dev/peps/pep-3132/
* ソートの安定性と複合的なソート -- Python公式ドキュメント
    - https://docs.python.org/ja/3/howto/sorting.html#sort-stability-and-complex-sorts
* Timsort -- 英語版Wikipedia
    - https://en.wikipedia.org/wiki/Timsort