ジェネレータ

ジェネレータはイテレータオブジェクトを返す関数だ。def を使って通常の関数と同じように定義できるけど、ひとつ違うのは return で値を返す代わりに、yield で計算途中の値を返すことができる点。ジェネレータから返されたイテレータオブジェクトは、nextが呼び出されると yield が出てくるまで計算を進めて値を返す。そしてまたnextが呼び出されると、続きの計算をして yield が出てきた時点でまた値を返す。これを繰り返すわけだ。だから、ジェネレータでは while ループの中で yield を使うのが常套手段、というかそうしないとうまく作れないんじゃないかな。

次の例はフィボナッチ数列を出力する。

def fib():
    a = 1
    b = 1
    while True:
        yield a
        a, b = b, a+b


i = fib()

for c in range(10):
    print i.next()

実行結果:

^o^ > python fib.py
1
1
2
3
5
8
13
21
34
55

うまくいった。この例では10個までしか出力してないけど、コードをよく見ればわかるとおり、終了しない無限ループになっている。言い換えると無限に値を生成できるってこと。これはリストやファイルオブジェクトから作るイテレータとは違うところだ。

イテレータ

Pythonのイテレータは外部イテレータといわれるやつみたい。
iter関数は、イテレート可能オブジェクトをイテレータオブジェクトに変換する。イテレータオブジェクトは次のような性質を持っている。

  • nextメソッドの呼び出しに対して「次の要素」を返す
  • 返す要素がなかったらStopIteration例外を発生する

リストをイテレータオブジェクトに変換して試してみよう。

>>> i = iter([1,2,3])
>>> i.next()
1
>>> i.next()
2
>>> i.next()
3
>>> i.next()
Traceback (most recent call last):
  File "", line 1, in 
StopIteration

イテレータオブジェクトiはnextに対して1~3を順に返し、返す要素がなくなった4回目にはStopIterationが発生している。

実際のプログラミングではforと一緒に使うことが多いようだ。

>>> i = iter(['foo','bar','baz'])
>>> for item in i:
...     print item
...
foo
bar
baz

ファイルオブジェクトもイテレート可能。nextに対して1行ずつ返すイテレータになる。

import sys

filename = sys.argv[1]
file = open(filename, "r")

i = iter(file)

for l in i:
  print l.rstrip("\n")

実行結果:

^o^ > type sample.txt
Mon
Tue
Wed
Thu
Fri
Sat
Sun

^o^ > python iterator.py sample.txt
Mon
Tue
Wed
Thu
Fri
Sat
Sun

実際には、for文では必要なら自動的にイテレータに変換してくれるので、次のようにも書ける:

file = open("sample.txt", "r")

for line in file:
    print line.rstrip("\n")

リスト内包表記

Pythonにはリスト内容表記がある。for~in~を使ってこうやって書く:

>>> [x*x for x in [1,2,3,4,5]]
[1, 4, 9, 16, 25]

表記こそ違うけどやってることはHaskellのリスト内包表記とおなじだ。

forは複数つけることもできる:

>>> [(x,y) for x in [1,2,3,4,5] for y in [1,2,3,4,5]]
[(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5),
 (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5),
 (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)]

これは次の二重のforと同じ。

>>> r = []
>>> for x in [1,2,3,4,5]:
...     for y in [1,2,3,4,5]:
...         r.append((x,y))
...
>>> r
[(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5),
 (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5),
 (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)]

さらに、ifを使って条件を制限できる:

>>> [(x,y) for x in [1,2,3,4,5] for y in [1,2,3,4,5] if x <= y]
[(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 2), (2, 3), (2, 4), (2, 5), (3, 3),
 (3, 4), (3, 5), (4, 4), (4, 5), (5, 5)]