静かなる名辞

pythonとプログラミングのこと


【python】組み込み関数all・anyの引数はできるだけジェネレータ式などで書く

概要

 組み込み関数all・anyはiterableの真理値すべてに対してand・orを計算します。

>>> all([True, True, False])
False
>>> any([True, True, False])
True

 このall, anyは引数を短絡評価してくれます。ただし、条件式をリスト内包表記などで書くと台無しになります。

ありがちな問題と改善方法

 たとえばこういうものについて考えます。

>>> all([x < 50 for x in range(10**4)])
False

 リストのすべての要素が50未満かどうかの判定ですね。

 問題は、リスト内包表記が返るまでにすべての比較(0 < 50~9999 < 50)が行われてしまうことです。これはいかにも無駄です。xが50に達した時点でFalseになるのはわかりますから。

 そこで、ジェネレータ式を使います。

>>> all(x < 50 for x in range(10**4))
False

 ジェネレータ式を見慣れていない方もいると思います。いきなり引数に内包表記の中身を書いているのでびびるかもしれませんが、本来はこういうものです。

>>> (x < 50 for x in range(10**4))
<generator object <genexpr> at 0x7ffaca359678>

 関数の唯一の引数として渡される場合は、かっこを省略してもジェネレータ式として解釈されます。

 詳細は以下のドキュメントなどを参照してください。

6. 式 (expression) — Python 3.7.2 ドキュメント
用語集 — Python 3.7.2 ドキュメント

 さて、このジェネレータ式は個々の要素の計算を取り出されるときまで遅延します。なので、無駄な計算を減らすのに使えます。

>>> import timeit
>>> timeit.timeit(lambda : all([x < 50 for x in range(10**4)]), number=10**2)
0.06413002900080755
>>> timeit.timeit(lambda : all(x < 50 for x in range(10**4)), number=10**2)
0.001326076002442278

 はるかに高速に実行が終わります。

まとめ

 all・anyの特性を理解していれば何ら不思議な事はありませんが、慣れていないとなんでもリスト内包表記で書く人が多いので、無意識に非効率的なコードを生成しているかもしれません。

 そういうときは、ここはジェネレータ式でいけるのではないか? ということを思い出すと効率的なコードを書けます。