Menu

PHPのジェネレータ:yield、yield fromとメモリ節約

ジェネレータは、yieldで値を1つずつ返す関数なので、foreachで配列を作らずに100万件をループできます。yield、キー、メモリの節約、無限の数列、yield from、send()とgetReturn()を学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

ジェネレータとは、yieldを含む関数です。呼び出しても本体は実行されず、foreachでループできるGeneratorオブジェクトが返ります。yield $valueはそれぞれ1つの値をループに渡し、ループが次を求めるまで関数を一時停止します。そのため値は、先に配列に集められるのではなく、1つずつ作られます。

出力はジェネレータとループの間を行き来します。各値は、ループが求めたときにだけ作られます。

ジェネレータはメモリを節約する

range(1, 1000000)は、ループが始まる前に100万個の整数の配列を作ります。同じ数をyieldするジェネレータは、一度に1つしか持ちません。実行して、それぞれに必要なメモリを比べてください。

どちらのループも同じ合計500000500000を出力しますが、配列版はextra memory 16.0 MB、ジェネレータはextra memory 0.0 MBと報告します。データが増えるほど差は広がります。大きなファイル、データベースの結果、APIのページにジェネレータが役立つのも同じ考え方からで、1件処理して捨て、次の1件を取るのです。

キーと値をyieldする

yield $key => $valueは、foreach ($gen as $key => $value)が受け取るキーを設定します。キーがなければ、ジェネレータはリストのように値に0から番号を振ります。よくある使い方は、行番号付きでファイルを1行ずつ読むことです。

メモリにあるのは一度に1行だけなので、同じ関数で2 GBのログファイルも扱えます。ここで使っているファイル関数はfopen()とfgets()で扱っています。

無限の数列と遅延パイプライン

ジェネレータは、使う側が求めるところまでしか実行されないので、永遠にループしてもかまいません。breakで止めるか、最初のいくつかの値を取る別のジェネレータで包みます。小さなジェネレータをつなげると、すべての段階が遅延評価されるパイプラインになります。

fibonacci()は自分では決して終わりませんが、プログラムは終了します。take()が求めた数のあとで要求をやめ、その先は何も計算されないからです。

yield from:別のジェネレータに任せる

yield fromは、配列、イテレータ、別のジェネレータのすべての値を受け渡します。ジェネレータを小さなものに分けたり、ネストした構造を再帰的にたどったりする方法です。

yield fromは内側のキーを保つので、どちらの配列もキー0と1をyieldします。そのためiterator_to_array()はaとbを上書きしてしまいます。値だけが重要なときは、2つ目の引数にfalseを渡しましょう。

send()とgetReturn()

ジェネレータは値を受け取ることもできます。$x = yield;で一時停止して待ち、$gen->send($value)はそのyieldの結果を$valueとしてジェネレータを再開します。またジェネレータは最終的な値をreturnでき、終わったあとにgetReturn()で読めます。

ほとんどのコードではsend()は必要なく、foreachでの遅延読み込みが日常的な使い方です。send()はコルーチンのライブラリを支える基本の部品です。

ジェネレータは1回しかループできない

ジェネレータは前に進むだけで、巻き戻せません。foreachで使い切ったあと、同じオブジェクトをもう一度ループすると例外が投げられます。

直すには、ループごとにジェネレータ関数を呼び直す(foreach (colors() as $c))か、本当に2回必要ならiterator_to_array(colors(), false)で一度だけ値を集めます。それを頻繁にしているなら、そのデータには普通の配列が正しい選択でした。

よくある質問

PHPのyieldは何をしますか?

yield $valueは、関数をループしているコードに値を1つ渡し、その場で関数を一時停止します。ループの次の繰り返しで、yieldのあとから再開します。yieldを含む関数は、呼び出しても実行されず、代わりにGeneratorオブジェクトを返します。

配列ではなくジェネレータを使うのはなぜですか?

配列はすべての要素を一度にメモリに持ちますが、ジェネレータは要素を1つずつ作ります。range(1, 1000000)をループすると100万個の整数がすべて確保されますが、それらをyieldするジェネレータならメモリは一定のままです。ジェネレータなら無限の数列も扱え、ループがbreakすれば処理も早めに止まります。

PHPのyield fromとは何ですか?

yield from $sourceは、別のジェネレータ、配列、Traversableのすべての値を、今のジェネレータ自身がyieldしたかのように受け渡します。式としては、内側のジェネレータの戻り値になります。

ジェネレータを2回ループできますか?

できません。ジェネレータは前にしか進みません。foreachが終わったあとにもう一度ループすると、Exception: Cannot traverse an already closed generatorが投げられます。新しいジェネレータを得るにはジェネレータ関数をもう一度呼ぶか、2回必要ならiterator_to_array()で値を保存します。

ジェネレータを配列に変換するには?

iterator_to_array($gen)は、ジェネレータのキーを保ったまますべての値を集めます。yield fromのあとなどでキーが重複すると、あとの値が前の値を上書きするので、普通のリストが欲しいときは2つ目の引数にfalseを渡します:iterator_to_array($gen, false)。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める