はじめに
このプログラムはジェネレータの使用例です。
CSVファイルから読み込んだデータを 1,000 行ずつ foreach で処理しています。
function readCsv()
{
$file = fopen("dummy.csv", "r");
$records = [];
while ($record = fgetcsv($file)) {
$records[] = $record;
if (count($records) >= 1000) {
yield $records;
$records = [];
}
}
}
foreach (readCsv() as $records) {
// 1,000 行ごとに実行される
}
どうして 1,000 行ごとに foreach の中の処理が実行されるの?
そもそも yield って何?
そう思った方に本記事を読んでいただけると幸いです。
ジェネレータとは?
PHP 5.5 から導入されたイテレータ(iterator)を簡単に実装できる機能で、反復処理においてメモリを節約しながら処理を実行できます。
イテレータの説明です。IT用語辞典 e-Wordsイーワーズ から引用しました。
イテレータとは、プログラミング言語の機能の一つで、配列のようなデータ構造の要素を順に走査していく繰り返し処理を簡潔に記述できる構文やオブジェクトなどのこと。
…とは言っても、分かりづらいので実際のプログラムを交えながら特徴を確認していきます。
特徴 ①
ジェネレータ関数は必要に応じて何度でも yield して値を返却することが可能です。
以下の例では、合計3回の yield をしています。
function colors()
{
echo "colors 開始\n";
echo "red\n";
yield "red"; // 1回目のyield
echo "green\n";
yield "green"; // 2回目のyield
echo "blue\n";
yield "blue"; // 3回目のyield
echo "colors 終了\n";
return "black";
}
$generator = colors();
foreach ($generator as $index => $value) {
echo "foreach " . ($index + 1) . "回目\n";
}
実行した結果、以下の出力が得られました。yield の度にジェネレータ関数の実行が一時停止して、foreach の中の処理が実行されていることがわかります。
colors 開始
red
foreach 1回目
green
foreach 2回目
blue
foreach 3回目
colors 終了
ちなみに、return した結果は foreach の繰り返し処理では取得されません。別途 getReturn()
を用いれば取得することが可能です。
echo $generator->getReturn();
// black
特徴 ②
ジェネレータを使うと、繰り返し処理(foreach)で処理するコードを書くときに、配列のサイズ分のメモリを確保しなくても済むようになります。
例として、10,000 行の CSV データを扱った場合のメモリ消費量を確認してみたいと思います。
読み込むファイルは以下のサイトを用いて作成しました。
まずは配列を用いた場合でメモリ消費量の最大値を確認してみます。以下のプログラムを実行しました。
function readCsv()
{
$records = [];
$file = fopen("dummy.csv", "r");
while ($record = fgetcsv($file)) {
$records[] = $record;
}
return $records;
}
echo "実行開始:" . round(memory_get_peak_usage() / (1024 * 1024), 3) . " MB\n";
$records = readCsv();
foreach ($records as $record) {}
echo "実行終了:" . round(memory_get_peak_usage() / (1024 * 1024), 3) . " MB\n";
実行した結果、以下の出力が得られました。
実行開始:0.419 MB
実行終了:10.361 MB
次にジェネレータを用いた場合でメモリ消費量の最大値を確認してみます。以下のプログラムを実行しました。
function readCsv()
{
$file = fopen("dummy.csv", "r");
while ($record = fgetcsv($file)) {
yield $record;
}
}
echo "実行開始:" . round(memory_get_peak_usage() / (1024 * 1024), 3) . " MB\n";
$generator = readCsv();
foreach ($generator as $record) {}
echo "実行終了:" . round(memory_get_peak_usage() / (1024 * 1024), 3) . " MB\n";
実行した結果、以下の出力が得られました。
実行開始:0.418 MB
実行終了:0.418 MB
実際に比較して、明らかにジェネレータを用いた場合のメモリ消費量が少ないことが確認できました。
おわりに
実際のプログラム例を交えながらジェネレータについて理解を深めてみました。使いこなすには難易度が高いかもしれませんが、メモリ使用量を節約できるのはメリットだと思います。利用が求められる場面もあるかもしれないので、頭の片隅に置いておいてはいかがでしょうか?