はじめに
- こんにちは、takadowaです。
- 前回の『Effective Python 第2版』 第1章<Pythonic思考>に引き続き、今回は第2章<リストと辞書>の内容をベースに、さらに調べて知ったことや自分の感想をまとめていきたいと思います。
実行環境
サンプルコードを掲載する際は、以下の実行環境で実行した結果を掲載します。
できる限りミスのない形で提供することを心がけますが、動かない、間違っているなどがありましてもご容赦ください。
- macOS 10.14.6
- Python 3.8.5
第2章<リストと辞書>
catch-allアンパック
通常のアンパックは a, b = [1, 2]
のように書きますが、この書き方だと値の数が一致しない場合、エラーが発生します。
# 通常のアンパックだと、左辺と右辺とで値の数が一致しない場合はエラーとなる
a, b = [1, 2, 3, 4]
>>>
...
ValueError: too many values to unpack (expected 2)
「いくつかのサンプルデータを見たところ2つだと思っていたが、実は3つ以上の値が入ることもあった」なんてことも開発しているとあるのではないでしょうか?愚直に実装すると以下のようになりますが、これは行数も多くなるし、バグにもなりやすいので良くありません。
# スライスやインデックスを使ったイケてない例
l = [1, 2, 3, 4]
a = l[0]
b = l[1]
c = l[2:]
そのようなときに便利なのが「catch-allアンパック」です。
# catch-allアンパックを使う
a, b, *c = [1, 2, 3, 4]
print(a, b, c)
>>>
1 2 [3, 4] # cに残りのすべてがリストで入っている
上記例のように、先頭に*
をつけた変数が残りのすべての値をリストで保持してくれるので、さきほどのような値の数が一致しないことによるエラーはなくなります。
また、以下のように、仮に要素が残っていなかったとしても、空リストになるので安心です。
# cに入る要素は残っていないがエラーにはならず、空リストが入る
a, b, *c = [1, 2]
print(a, b, c)
>>>
1 2 [] # cは空リストになる
このcatch-allアンパックは、最後だけでなく、先頭や真ん中に指定することも可能なので、状況に応じて使い分けることが可能です。
# 先頭で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に書かれているようです。*a = range(5)
はエラーになるが、*a, = range(5)
はOKな書き方だそうで、へぇ〜ってなりました。
Pythonのソートは安定ソートである
ソートの安定性と複合的なソート -- Python公式ドキュメント に
ソートは、 安定 (stable) であることが保証されています。これはレコードの中に同じキーがある場合、元々の順序が維持されるということを意味します。
と書かれているとおり、Pythonのソートは安定ソートだそうです。また、具体的に利用されているアルゴリズムも以下のように記されています。
Python では Timsort アルゴリズムが利用されていて、効率良く複数のソートを行うことができます、これは現在のデータセット中のあらゆる順序をそのまま利用できるからです。
ちなみに、Timsortを英語版Wikipediaで調べると、実装したのはTim Petersという方だそうで、Tim Petersは前回の記事でも紹介した「The Zen of Python」を書いた人でもあります。私はこの方を今回初めて知りましたが、Pythonに詳しい方ならみんな知っているのでしょうか??
dictの挿入順序について
Python3.5以前では、dictをイテレートすると任意の順序でキーが返されるようになっていました。
(中略)
Python3.6、Python3.7以降は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が発生するのはよくご存知のことだと思います。
# 存在しないキーにアクセスしたらKeyErrorが出る
d = {"a": 1}
print(d["b"])
>>>
...
KeyError: 'b'
この存在しないキーにアクセスしたときの振る舞いを実装できるのが特殊メソッド__missing__
で、以下のようにdictのサブクラスとして定義します。
# __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版
参考サイト
- PEP 3132 -- Extended Iterable Unpacking
- ソートの安定性と複合的なソート -- Python公式ドキュメント
- Timsort -- 英語版Wikipedia