読者です 読者をやめる 読者になる 読者になる

ジェネレーターとCPython(上)

Python

 前回書いた、シーケンスの一部を反転するという記事に関して、cozeさんから非常に示唆に富んだコメントを頂きました。

ところで「なぜなら、generatorを渡しても、結局、内部でtupleに変換されるので、この分のオーバーヘッドが生じるためです。」の部分ですが、もしジェネレータが内部でタプルに変換されるなら、
def one():
__while 1:
____yield 1
>>> all(one())
がメモリを食い尽くさないのは何故ですか? ジェネレータがタプルに変換されるのならば、1を要素にもつ巨大(サイズ無限大)なタプルが生成されると思うのですが…

「generatorを渡しても、結局、内部でtupleに変換される」という表現が、generatorは「全て」CPython実装内部でtupleに変換される、と主張しているように誤解を与えてしまったかもしれませんが、僕が主張したかったのは、「generatorを使う側(消費者側)の結果が、generator(生産者側)の返す結果の『全て』に依存している場合には、現在のCPythonの実装ではlist/tupleに変換される」ということです。
 実際、消費者が、生産者の活動終了を待たずに消費活動を停止できるようなケースでは、list/tupleへの変換は行われません。


 具体例をあげて説明します。cozeさんの例にあるような、常に1を返すgeneratorについて考えてみましょう。

def one():
  while 1:
    yield 1

any

>>> any(one())
True

 このケースでは、listへの変換は行われず、したがって、無限リストも生成されません。StopIterationが排出されるまで、nextメソッドを使って、要素が一つ一つ取り出されます。
 なぜそのような実装になっているかと言うと、ジェネレーターが、真と評価できる値を一つでも返した時点で、消費者(any)は処理を打ち切ることができるからです。

all

>>> all(one())

 このケースは、無限ループに入って処理は終了しませんが、やはりlistへの変換は行われません。理由はanyとほぼ同じです。

map

>>> map(func, one())

 このケースでは、MemoryErrorで停止するまで、限りなくlistを伸張しようとします。ジェネレーターの返す値「全て」に、与えられた関数funcを適用しなくてはならないため、「まず適用対象の値のリストを構築し終えてから、次にそれらに関数を適用する」という実装が選択されています。
 このような実装が選択されている理由は、後で考察してみます。

sum

>>> sum(one())

 このケースは、消費者(sum)が、ジェネレーターの返す値の全てを必要とするという点ではmapと似ているのですが、実際には、ジェネレーターが非数値を返した時点で例外を排出して処理を終了できるので、any, allと同じような振る舞いをします。つまり、listへの変換は行われません。


 続く。