2014年9月27日土曜日

Rubyistが歩んでみる蛇の道はPython その7

Dive into Python 3に戻って、chapter 7と8からイテレータの話。

オブジェクトobjをlistやtupleのようにforでぶん回せる(iterableである)ようにするには、

  1. objが__iter__()メソッドを持っている
  2. obj.__iter__()が返すオブジェクトが__next__()メソッドを持っている
ようにすれば良い。大方の想像通り、iter(xx)するとxx.__iter__()が、next(yy)するとyy.__next__()が実行される。__next__()は、呼ばれる度に列挙される値を1つずつ返すようにする。また、列挙し終えたならStopIteration例外を投げる。端落ちしてもNoneが返されるので、列挙の終了とはみなされない。

では、generatorはどうしてiterableだったのか。別に特別扱いされていたわけでも何でもなく、generatorも上の条件を満たしていたのだ。

>>> def gen():
...   yield 0
...
>>> g = gen()
>>> g.__iter__
<method-wrapper '__iter__' of generator object at 0x10a88cf78>
>>> g == g.__iter__()
True
>>> g.__next__
<method-wrapper '__next__' of generator object at 0x10a88cf78>
>>> g.__next__()
0
>>> g.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
generatorは実は、自分自身を返す__iter__()と、yieldした値を返す__next__()を持っていたのだ。なおgeneratorは、StopIterationを明示的にを投げなくてもreturnするときに投げてくれるようだ。普通の関数と違って、yieldとreturnを区別できるからだろうね。

ちなみに、普通のlistやtupleも例外ではなく、__next__()を持っているオブジェクトを返す__iter__()を持っている。

この緩い条件を満たしたiterableなオブジェクトから、別のiterableなオブジェクトを生成するitertoolsというライブラリが、Pythonには標準で付いている。chapter 8ではitertoolsの中からいくつかの関数を使って見せているけど、詳しくはhelp()で調べてみれば良いだろう(きちんとdocstringが用意されているって、地味に凄いことだよなぁ)。…と、これだけで片付けるのはあんまりなので、iteratorのありがたみを3行で表す(パッと見2行だけど、printの次の空行を含めて3行)。

for p in itertools.permutations(range(10000)):
  print(p)
順列を列挙するコードを自前で書かずに済むのはありがたいけど、それ以前にこのコードが動くこと自体がありがたい(実行に何年かかるか知らないが)。ちょっと考えれば分かるように、10000要素の整数リストを10000!個持つリストを作ってからループを回そうなんて思ったら、どう考えてもメモリが足りない。それでもこのコードが延々と順列を表示し続けてくれるのは、いきなり10000!通り全てを生成しようなんて馬鹿げたことをせず、next()の度に1つずつ列挙してくれているからだろう。ここらへんのケアをおまかせできるのは美味しい。

0 件のコメント:

コメントを投稿