20.

PHP の yield(ジェネレータ)|メモリを使わない繰り返しと yield from

編集
この記事の要点
  • yield を含む関数はジェネレータになり、呼んでも中身はまだ実行されない
  • 値を1 件ずつ作って返すので、100 万件でもメモリを使わない
  • 戻り値は Generatorforeach で回せるが、配列ではないので count() は使えない
  • yield $key => $value でキー付き、yield from で別のジェネレータを繋げられる
  • 一度しか回せない。2 回使うなら iterator_to_array() で配列にする

配列を返す場合との違い

// 配列を作って返す: 100 万件ぶんのメモリが必要
function rangeArray(int $n): array
{
    $result = [];
    for ($i = 0; $i < $n; $i++) {
        $result[] = $i;
    }
    return $result;
}

// ジェネレータ: 1 件ずつ作る
function rangeGen(int $n): Generator
{
    for ($i = 0; $i < $n; $i++) {
        yield $i;
    }
}

foreach (rangeGen(1_000_000) as $i) {
    if ($i > 3) break;
    echo $i;          // 0 1 2 3   4 件目以降は作られない
}
$before = memory_get_usage();
$a = rangeArray(1_000_000);
echo (memory_get_usage() - $before) / 1024 / 1024;   // 数十 MB

$before = memory_get_usage();
$g = rangeGen(1_000_000);
echo (memory_get_usage() - $before);                 // ほぼ 0

呼んだ時点では実行されない

function gen(): Generator
{
    echo "開始\n";
    yield 1;
    echo "途中\n";
    yield 2;
    echo "終了\n";
}

$g = gen();          // 何も出力されない(Generator を作っただけ)
echo "作成した\n";

foreach ($g as $v) { // ここで初めて中身が動く
    echo "受け取り: {$v}\n";
}
// 作成した / 開始 / 受け取り: 1 / 途中 / 受け取り: 2 / 終了

yield のところで実行が一時停止し、次の値が要求されたときに続きから再開します。この「途中で止まる」性質がジェネレータの本質です。

大きなファイルを読む

function readLines(string $path): Generator
{
    $fp = fopen($path, 'r');
    if ($fp === false) {
        throw new RuntimeException("開けません: {$path}");
    }
    try {
        while (($line = fgets($fp)) !== false) {
            yield rtrim($line, "\r\n");
        }
    } finally {
        fclose($fp);        // 途中で break しても必ず閉じる
    }
}

foreach (readLines('huge.csv') as $i => $line) {
    if ($i === 0) continue;      // ヘッダーを飛ばす
    // 1 行ずつ処理する
}

file()file_get_contents()ファイル全体をメモリに載せます。数 GB の CSV では Allowed memory size exhausted になるため、この形で 1 行ずつ処理します。finally を付けておくと、途中で break してもファイルが閉じられます。

キー付きの yield

function pairs(): Generator
{
    yield 'name' => '田中';
    yield 'age'  => 30;
}

foreach (pairs() as $key => $value) {
    echo "{$key}: {$value}\n";
}

// キーを省略すると 0 から自動採番される
function nums(): Generator
{
    yield 'a';      // キー 0
    yield 'b';      // キー 1
    yield 5 => 'c'; // キー 5
    yield 'd';      // キー 6
}

yield from で繋げる

function inner(): Generator
{
    yield 1;
    yield 2;
    return 'inner の戻り値';
}

function outer(): Generator
{
    yield 0;
    $result = yield from inner();   // inner の値をそのまま流す
    yield 3;
    echo $result;                   // inner の戻り値
}

foreach (outer() as $v) { echo $v; }   // 0 1 2 3

// 配列も流せる
function withArray(): Generator
{
    yield from [10, 20];
    yield 30;
}

yield from はキーをそのまま引き継ぎます。複数のジェネレータを繋げるとキーが重複するため、iterator_to_array() するときは第 2 引数を false にしてキーを捨ててください。

戻り値と send()

function counter(): Generator
{
    $count = 0;
    while (true) {
        $add = yield $count;    // send() で渡された値を受け取る
        if ($add === null) break;
        $count += $add;
    }
    return $count;              // getReturn() で取れる
}

$g = counter();
echo $g->current();     // 0
echo $g->send(5);       // 5
echo $g->send(3);       // 8
$g->send(null);         // ループを抜ける
echo $g->getReturn();   // 8
メソッド働き
current()いまの値を返す(進めない)
key()いまのキー
next()次まで進める
valid()まだ値があるか
send($v)値を渡して次まで進め、その値を返す
getReturn()最後まで回したあとreturn の値を取る

getReturn() を途中で呼ぶと Exception: Cannot get return value of a generator that hasn't returned になります。

制約と注意点

$g = rangeGen(3);

// count() は使えない(Countable ではない)
// echo count($g);                    // TypeError
echo iterator_count(rangeGen(3));     // 3   ただし全部消費する

// 添字アクセスもできない
// echo $g[0];                        // Error

// 一度回すと再利用できない
foreach ($g as $v) {}
foreach ($g as $v) { echo $v; }       // Exception: Cannot traverse an already closed generator

// 2 回使いたいなら配列にする(メモリの利点は失われる)
$array = iterator_to_array(rangeGen(3));

// 途中で巻き戻すこともできない
// $g->rewind();                      // 開始後は Exception

Generator を返す関数の型宣言

function gen1(): Generator { yield 1; }   // 具体的
function gen2(): iterable  { yield 1; }   // 配列でもジェネレータでもよい

function total(iterable $rows): int       // 受け取る側は iterable にしておくと柔軟
{
    $sum = 0;
    foreach ($rows as $r) { $sum += $r; }
    return $sum;
}

echo total([1, 2, 3]);        // 6
echo total(rangeGen(4));      // 6

引数の型は array ではなく iterable にしておくと、配列とジェネレータの両方を受け取れます。

使いどころのまとめ

状況選ぶもの
件数が少なく、何度も使う配列
件数が多い / 不明(ファイル・DB・API のページ送り)ジェネレータ
途中で打ち切ることがあるジェネレータ
ソートや count() が必要配列
無限に続く列(連番・繰り返し)ジェネレータ

関連

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 基本事項
  2. HTMLへの埋め込み
  3. 変数
  4. 可変変数
  5. 定数
  6. データ型
  7. キャスト
  8. エスケープ文字
  9. 配列
  10. 演算子
  11. 代入の際の注意点
  12. 条件分岐
  13. 繰り返し処理
  14. クラスとインスタンス
  15. コンストラクタ
  16. 関数
  17. スーパーグローバル変数
  18. スコープ
  19. staticについて
  20. yieldについて
  21. ファイルのアップロード方法
  22. DB接続方法
  23. SQL実行方法
  24. カプセル化の具体例
  25. 継承の構文
  26. オーバーライド
  27. ポリモーフィズム(多様性)の具体例
  28. 抽象クラス・メソッドの構文と具体例
  29. GET通信
  30. try catchで全てのエラーを拾う方法

最近更新/作成されたページ