静かなる名辞

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


【python】ジェネレータ式の使い所

概要

 ジェネレータ式を使っているコードを見かける機会は少ないですが、ケースによっては有用なので使い所を紹介します。

 この記事を読むと、漫然と使われたリスト内包表記に対して「ジェネレータ式の方が良くない?」と言えるようになったりします。

ジェネレータ式とは?

 ご存知の方も多いと思いますが、ジェネレータ式はリスト内包表記の外側の角括弧を丸かっこにしたようなものです。ジェネレータイテレータが返ります。

>>> l = [x for x in range(3)] # 通常のリスト内包表記
>>> l
[0, 1, 2]
>>> g = (x for x in range(3)) # ジェネレータ式
>>> g # ジェネレータオブジェクトが返る
<generator object <genexpr> at 0x7f512d98e0a0>
>>> list(g)  # listに変換するとこんな感じ
[0, 1, 2]

 関数の唯一の引数として呼び出される場合は外側の丸かっこを省略できるという構文上の決まりがあります。

>>> f((x for x in range(3)))  # これでもいいが、かっこが多くてかっこ悪い
<generator object <genexpr> at 0x7f512d98e0a0>
>>> f(x for x in range(3))  # かっこが少なくてかっこ良い
<generator object <genexpr> at 0x7f512d98e0a0>

関数の唯一の引数として渡す

 上にも書いたような省略構文があるということ自体が、そういう使い方を意図しているということを表します。

 どんな関数に対して有用かと言うと、リスト内包表記の結果生成されるlistがなくても構わないような処理をする関数に対して威力を発揮します。逆に言うと、それ以外で使うのは反則です。

 たとえばsum, max, minなどが良いでしょう。これらはいずれもシーケンスを一回走査すれば処理が終わります。リストである必要性はありません。

>>> sum([x**2 for x in range(10**6)])
333332833333500000
>>> sum(x**2 for x in range(10**6))
333332833333500000
>>> import timeit
>>> timeit.timeit(lambda : sum([x**2 for x in range(10**6)]), number=10)
2.786539091001032
>>> timeit.timeit(lambda : sum(x**2 for x in range(10**6)), number=10)
2.8657835299964063

 なぜか遅くなりましたが・・・何回か測ってもジェネレータ式の方が遅いですね(python3.6 on linux)。理由はわかるようなわからないような感じです。擁護しておくと、これで瞬間的なメモリ消費は確実に減ります。

 こういうこともあるので、気をつけましょう。

途中で処理を打ち切りたいときに使う

 ジェネレータ式はジェネレータなので、要素を取り出すたびに計算を行います。ということは、途中で打ち切るので全体を先に計算してしまうと無駄が多いようなケースで使うと威力を発揮します。break的に使えます。

>>> def f1():
...     for x in [x**2 for x in range(10**4)]:
...         if x > 100:
...             break
... 
>>> def f2():
...     for x in (x**2 for x in range(10**4)):
...         if x > 100:
...             break
... 
>>> timeit.timeit(f1, number=100)
0.2562638619856443
>>> timeit.timeit(f2, number=100)
0.00045182398753240705

 無理やり書いてみましたが、確かに速いけど、例がいまいちな気がします。forの引数に直接書くくらいならforの中であれこれやれば良い訳で。

 なので、こちらも関数の引数に渡すオブジェクトでそういう動作をさせたいときに使うと良いです。その好例にall・anyで使う場合などがあります。これについては以前記事にしました。

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

>>> all([x < 50 for x in range(10**4)])  # ぜんぶ比較するので無駄
False
>>> all(x < 50 for x in range(10**4))  # 途中で条件を満たさなくなった時点で打ち切られる
False

 受け取る関数がある程度配慮していれば、大変便利に使えます。

まとめ

 要するに「リストオブジェクトを作る必要がなく、何らかのイテレータがあれば良い」ようなときに使えます。まとめて言えばそれだけです。

 ジェネレータ式を使うとエレガントなケースが割とありますので、リスト内包表記を見たらジェネレータ式にできないか少し考えてみましょう。