python

pythonのgeneratorをちゃんと理解する日がきました

投稿日:

この記事を読めば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))

そこで

python-izmでgeneratorの基本を見てみると

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怖くないわ

以上です

-python

Copyright© CTOを目指す日記 , 2024 All Rights Reserved.