この記事を読めばgeneratorが怖くなくなります。すくなくとも私はそうでした。
で、次のようなことが学べます。
・generatorは反復可能なオブジェクトで配列などとの違いはメモリがO(1)しか必要ない
・generator can not be rewound、巻き戻せません
・generatorはstateを保持する
・操作編、 next(<generator>)で次の値がyieldできます
でははじめます。
早速ですが
次の文で、[]を外すと早くなるでしょうか?
ans = any([ row['a'] > 20 for row in rows if 'a' in row ])
すこし考えてみてください
答えは
遅くなります。
これ、実は実験する前はgenerator 返したほうが速くなるとおもってたんですよね。
でも結果は以外
ほぼ同じです。むしろ、若干遅めという結果になりました。
フルのテストと結果はこちら
#-*- coding: utf-8 -*- import time from collections import defaultdict def timeit(method): def timed(*args, **kw): global calc calc = defaultdict(int) print ('===========') ts = time.time() result = method(*args, **kw) te = time.time() print ('%r %2.8f ms' % (method.__name__, (te - ts) * 1000)) print("calc %s times" % sum(calc.values())) return result return timed import random @timeit def with_generator(rows): ans = any( row['a'] > 20 for row in rows if 'a' in row ) return ans @timeit def with_list(rows): ans = any([ row['a'] > 20 for row in rows if 'a' in row ]) return ans for i in range(5): N = 40 * 10**i rows = [ dict(zip(('c', 'b', 'a'), (x, y, z))) for x, y, z in zip( range(1, N+1), range(1569488750, 1569488750 + (100*N), 100), [ random.uniform(0, 10) for _ in range(N)] ) ] print('-------- N:%s --------' % N) with_generator(rows) #with_list(rows)
そして結果は
(py37new) > python generator_or_list_faster.py -------- N:40 -------- =========== 'with_list' 0.00572205 ms calc 0 times -------- N:400 -------- =========== 'with_list' 0.03480911 ms calc 0 times -------- N:4000 -------- =========== 'with_list' 0.37598610 ms calc 0 times -------- N:40000 -------- =========== 'with_list' 3.55100632 ms calc 0 times -------- N:400000 -------- =========== 'with_list' 41.69225693 ms calc 0 times (py37new) > python generator_or_list_faster.py -------- N:40 -------- =========== 'with_generator' 0.00596046 ms calc 0 times -------- N:400 -------- =========== 'with_generator' 0.03933907 ms calc 0 times -------- N:4000 -------- =========== 'with_generator' 0.39196014 ms calc 0 times -------- N:40000 -------- =========== 'with_generator' 3.76701355 ms calc 0 times -------- N:400000 -------- =========== 'with_generator' 44.31986809 ms calc 0 times
という結果で5%から10%遅いんです。
とまぁ
こういう疑問から実験を通して
一回アカデミックにgeneratorを学んで、今回の結果に合点がいくのか
そしてこれらのpythonライフにgeneratorを怖がらずに適切に使えるようになる
を目的に記事を書いていきます。
まずはgeneratorってなんですか?
今の知識だとなんかiteratorっぽくてyieldつかうやつなんじゃないの?
def odd_generator(a_number): for n in range(a_number): if n % 2 == 1: yield n
とipythonでしてみてください
すると
In [3]: odd_generator(7) Out[3]: <generator object odd_generator at 0x10ec36dd0>
かえってきたオブジェクトはgeneratorと謳っております
で、かえってきたgeneratorをgnという変数に入れておいて、以下のようにしてみるとlistだけ、直感的に納得できる結果で
gnをforで回しても内部の要素がとれないんだ、とわかります。
In [7]: list(gn) Out[7]: [1, 3, 5] In [8]: for n in gn: ...: print (n) ...: In [9]: for n in gn: ...: print (type(n))
そこで
generatorは反復可能なオブジェクトで配列などとの違いはメモリがO(1)しか必要ない点です。
はい。
この時点で、最初の私の実験でgeneratorのほうが遅かった理由がわかりました。
つまり、40万程度の反復を行ったわけですが、メモリの効率はgeneratorのほうがよかったもののcpu速度は変わらなかった
むしろ、その都度呼び出すめっちゃ軽いけどoverheadの積み重ねでlistよりも少し遅かった
ということがわかりました。キラリ
また、上の例でgnが何もprintしなかったのたのはgeneratorは一回消費しきってしまうと、それで終わり。使い切り配列みたいなものとおもえばいいですかね。
だから、ちゃんと生成したてのgeneratorは手つかずの状態ですからほら、
for n in odd_generator(7): ...: print(n) ...: 1 3 5
ちゃんとでますね。
でもほら、
In [15]: gn = odd_generator(7) In [16]: for n in gn: print(n) 1 3 5 In [17]: for n in gn: print(n) # 上で絞りきっちゃったので、なにもでないよ In [18]:
ということです。
配列とちがって、こういう特性もあるのですね。
このgeneratorって元に戻せないのかなー、gn.reset()みたいにすると、もう一度先頭のデータからアクセスできたり
resetting-generator-object-in-python それはできないみたいです。
gn, gn_copied = itertool.tee(gn)
みたいにするworkaroundはあるようです。ということで
generator can not be rewound、巻き戻せません、ということです
次、小技、紹介、操作編。
二行目のgn.next()って、methodはないんですね(汗) 代わりに、next(<generator>)で次の値がyieldできます
In [18]: gn = odd_generator(7) In [19]: gn.next() --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-19-495dff2f4ab5> in <module> ----> 1 gn.next() AttributeError: 'generator' object has no attribute 'next' In [20]: next(gn) Out[20]: 1 In [21]: next(gn) Out[21]: 3 In [22]: next(gn) Out[22]: 5 In [23]: next(gn) --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-23-af22cb28d57a> in <module> ----> 1 next(gn) StopIteration:
ここで、generatorの特徴をこちらの記事の表現を表すと
generatorはstateを保持するclassみたいなもん、ということです。
まとめ
・generatorは反復可能なオブジェクトで配列などとの違いはメモリがO(1)しか必要ない
・generator can not be rewound、巻き戻せません
・generatorはstateを保持する
・操作編、 next(<generator>)で次の値がyieldできます
といことでgeneratorとはlistと違ってメモリO(1)で反復できる(statefulな)オブジェクトでした
あー、もうgenerator怖くないわ
以上です