]> Panopticon :: Python :: Pythonのgenerator

<< Haskellはじめました(2) Pythonでfoldl, foldr | main | Pythonのgenerator(2) グラフ探索(1) >>

Pythonのgenerator

ジェネレータというものを試してみようと思った。

通常の関数の場合、

def hello():
    return 'hello,world!'
>>> hn = hello()
>>> hn
'hello,world!'

実行すると文字列が返ってきます。その結果、hnは文字列を指すことになります。

ジェネレータだと、

def hellogen():
    yield 'h'
    yield 'e'
    yield 'l'
    yield 'l'
    yield 'o'
    yield ','
    yield 'w'
    yield 'o'
    yield 'r'
    yield 'l'
    yield 'd'
    yield '!'

>>> hg = hellogen()
>>> hg
<generator object at 0x00D2CB20>

なんかオブジェクトが作られたようです。関数定義内にyieldが含まれる場合、それはジェネレータとして扱われます。ジェネレータは実行されるとgenerator objectを生成し、それがhgの参照となっているようです。このgenerator objectに対して、

>>> hg.next()
'h'

とnextメソッドを実行するとhが返ってきます。これは一行目のyieldの引数ですね。

>>> hg.next()
'e'
>>> hg.next()
'l'
>>> hg.next()
'l'
>>> hg.next()
'o'

next()を続けると、そのたびに一文字ずつ、別の文字が返ってきます。これらはhellogen()の中でyieldの引数として与えたものです。generator objectは、next()が実行されるたびにyieldの引数をひとつずつ順番に返します。generrator objectは「次によばれたとき、どこから開始すればよいか」を記憶しているようです。

>>> hg1 = hellogen()
>>> hg2 = hellogen()
>>> hg1.next()
'h'
>>> hg1.next()
'e'
>>> hg2.next()
'h'
>>> hg1.next()
'l'

hellogen()によってふたつのgenerator objectを作ってみます。それぞれの「次の開始位置」は別々に記憶されているのがわかります。hg1とhg2は別のインスタンスだということです。

>>>  hg1 is hg2
False

上のhellogen()によって作られたgenerator objectは、「12個の文字を順番に返すという約束」を持っています。12個を超えて文字を取り出そうとするとエラーになります。

...
>>> hg.next()
'!'
>>> hg.next()

Traceback (most recent call last):
  File "<pyshell#40>", line 1, in <module>
    hg.next()
StopIteration

ジェネレータはfor文で回してやることができます。

>>>  for t in hellogen():
	print t,
	
h e l l o , w o r l d !

これは、

>>>  for t in hello():
	print t,
	
h e l l o , w o r l d !

と同じように見えます。つーか結果としては同じで、文字が用意されるタイミングが違います。hello()のほうは、forループに入った瞬間に'hello,world!'の文字列全体が用意され、そこから一文字ずつ取り出してprintしています。hellogen()のほうは、まずhellogen()によって'h'一文字だけが用意され、それを取り出して表示、また次の文字が用意され、それを取り出して表示、と一文字ずつ取得と表示を行っています。

極端な例だと

def gentest(n):
    i = 0
    while i < n:
        yield i ** 2
        i += 1

def test1():
    for t in [x ** 2 for x in range(1000000)]:
        if (t % 10000000000 == 0):
            print t
    return

def test2():
    for t in gentest(1000000):
        if (t % 10000000000 == 0):
            print t
    return

こんな感じで大量に調べなければならない値があってかつ候補のほとんどがフィルタされて消えるときには、range()で候補のリストをまとめて用意するのはメモリの無駄ってことでジェネレータが有効ですね。

上のgentest(n)の引数をとっぱらうと

def gentest():
    i = 0
    while 1:
        yield i ** 2
        i += 1

0, 1, 4, 9, 16, ...と無限に要素を返し続けるジェネレータが作れます。

定番のFibonacci級数

def fibs():
    a = 0
    b = 1
    while 1:
        yield a
        a, b = b, a+b

>>>  for t in xrange(15):
    print f.next(),

0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

Schemeでいうstreamのようなものなので、SICPを真似てちょっと不思議なこともできる。

def fibs2():
    yield 0
    yield 1
    f = fibs2()
    g = fibs2()
    g.next()
    while 1:
        yield f.next()+g.next()

>>>  f2 = fibs2()
>>>  for t in xrange(15):
    print f2.next(),

0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

グラフ探索とかもできそう。

カテゴリ

Trackback URI

http://www.panopticon.jp/mt/mt-tb.cgi/53

Trackbacks(0)

コメントする