ジェネレータとは

(PHP 5 >= 5.5.0, PHP 7)

ジェネレータを使えば、シンプルな イテレータを簡単に実装できます。 Iterator インターフェイスを実装したクラスを用意する オーバーヘッドや複雑さを心配する必要はありません。

ジェネレータを使うと、foreach でデータ群を順に処理するコードを書くときに メモリ内で配列を組み立てなくても済むようになります。 メモリ内で配列を組み立てると memory_limit を越えてしまうかもしれないし、 無視できないほどの時間がかかってしまうかもしれません。 配列を作る代わりに、ジェネレータ関数を書くことになります。これは通常の 関数と同じものですが、 ジェネレータ関数は一度だけ return するのではなく、必要に応じて何度でも yield することができます。 つまり、値を繰り返し返せるということです。

シンプルな例として、range() 関数をジェネレータで実装しなおしてみましょう。 標準の range() 関数は、すべての値を含む配列を作ってそれを返します。 結果的に、かなり大きな配列になる可能性があります。たとえば range(0, 1000000) を実行すると、 100 MB を超えるメモリを使うことになります。

その代替として、ジェネレータ xrange() を実装します。 必要なメモリは、Iterator オブジェクトを作ってジェネレータの内部の状態を記録しておくのに必要なものだけになります。 1 KB 未満で収まるでしょう。

例1 ジェネレータを使った range() の実装

<?php
function xrange($start$limit$step 1) {
    if (
$start $limit) {
        if (
$step <= 0) {
            throw new 
LogicException('Step must be +ve');
        }

        for (
$i $start$i <= $limit$i += $step) {
            
yield $i;
        }
    } else {
        if (
$step >= 0) {
            throw new 
LogicException('Step must be -ve');
        }

        for (
$i $start$i >= $limit$i += $step) {
            
yield $i;
        }
    }
}

/*
 * 次の例で、range() と xrange() が同じ結果を返すことに注目しましょう
 */

echo 'Single digit odd numbers from range():  ';
foreach (
range(192) as $number) {
    echo 
"$number ";
}
echo 
"\n";

echo 
'Single digit odd numbers from xrange(): ';
foreach (
xrange(192) as $number) {
    echo 
"$number ";
}
?>

上の例の出力は以下となります。

Single digit odd numbers from range():  1 3 5 7 9 
Single digit odd numbers from xrange(): 1 3 5 7 9 

Generator オブジェクト

ジェネレータ関数を最初に呼んだときには、内部クラス Generator のオブジェクトを返します。 このオブジェクトは Iterator インターフェイスを実装しており、 前進しかできないイテレータオブジェクトと同じように振る舞います。 そして、このオブジェクトが提供するメソッドを使えば、 値を送ったり戻したりなどしてジェネレータの状態を操作できます。

add a note add a note

User Contributed Notes 8 notes

up
108
bloodjazman at gmail dot com
6 years ago
for the protection from the leaking of resources
see RFC https://wiki.php.net/rfc/generators#closing_a_generator

and use finnaly

sample code

function getLines($file) {
    $f = fopen($file, 'r');
    try {
        while ($line = fgets($f)) {
            yield $line;
        }
    } finally {
        fclose($f);
    }
}

foreach (getLines("file.txt") as $n => $line) {
    if ($n > 5) break;
    echo $line;
}
up
19
montoriusz at gmail dot com
3 years ago
Bear in mind that execution of a generator function is postponed until iteration over its result (the Generator object) begins. This might confuse one if the result of a generator is assigned to a variable instead of immediate iteration.

<?php

$some_state
= 'initial';

function
gen() {
    global
$some_state;

    echo
"gen() execution start\n";
   
$some_state = "changed";

    yield
1;
    yield
2;
}

function
peek_state() {
    global
$some_state;
    echo
"\$some_state = $some_state\n";
}

echo
"calling gen()...\n";
$result = gen();
echo
"gen() was called\n";

peek_state();

echo
"iterating...\n";
foreach (
$result as $val) {
    echo
"iteration: $val\n";
   
peek_state();
}

?>

If you need to perform some action when the function is called and before the result is used, you'll have to wrap your generator in another function.

<?php
/**
  * @return Generator
  */
function some_generator() {
    global
$some_state;

   
$some_state = "changed";
    return
gen();
}
?>
up
15
info at boukeversteegh dot nl
3 years ago
Here's how to detect loop breaks, and how to handle or cleanup after an interruption.

<?php
   
function generator()
    {
       
$complete = false;
        try {

            while ((
$result = some_function())) {
                yield
$result;
            }
           
$complete = true;

        } finally {
            if (!
$complete) {
               
// cleanup when loop breaks
           
} else {
               
// cleanup when loop completes
           
}
        }

       
// Do something only after loop completes
   
}
?>
up
18
dc at libertyskull dot com
5 years ago
Same example, different results:

----------------------------------
           |  time  | memory, mb |
----------------------------------
| not gen  | 0.7589 | 146.75     |
|---------------------------------
| with gen | 0.7469 | 8.75       |
|---------------------------------

Time in results varying from 6.5 to 7.8 on both examples.
So no real drawbacks concerning processing speed.
up
19
lubaev
5 years ago
Abstract test.
<?php

$start_time
=microtime(true);
$array = array();
$result = '';
for(
$count=1000000; $count--;)
{
 
$array[]=$count/2;
}
foreach(
$array as $val)
{
 
$val += 145.56;
 
$result .= $val;
}
$end_time=microtime(true);

echo
"time: ", bcsub($end_time, $start_time, 4), "\n";
echo
"memory (byte): ", memory_get_peak_usage(true), "\n";

?>

<?php

$start_time
=microtime(true);
$result = '';
function
it()
{
  for(
$count=1000000; $count--;)
  {
    yield
$count/2;
  }
}
foreach(
it() as $val)
{
 
$val += 145.56;
 
$result .= $val;
}
$end_time=microtime(true);

echo
"time: ", bcsub($end_time, $start_time, 4), "\n";
echo
"memory (byte): ", memory_get_peak_usage(true), "\n";

?>
Result:
----------------------------------
           |  time  | memory, mb |
----------------------------------
| not gen  | 2.1216 | 89.25      |
|---------------------------------
| with gen | 6.1963 | 8.75       |
|---------------------------------
| diff     | < 192% | > 90%      |
----------------------------------
up
0
chung1905 at gmail dot com
10 days ago
In addition to the note of "montoriusz at gmail dot com": https://www.php.net/manual/en/language.generators.overview.php#119275

"If you need to perform some action when the function is called and before the result is used, you'll have to wrap your generator in another function."
You can use Generator::rewind instead (https://www.php.net/manual/en/generator.rewind.php)

Sample code:
<?php
/** function/generator definition **/

echo "calling gen()...\n";
$result = gen();
$result->rewind();
echo
"gen() was called\n";

/** iteration **/
?>
up
-7
youssefbenhssaien at gmail dot com
2 years ago
A simple function to parse an ini configuration file
<?php
   
function parse_ini($file_path){
        if(!
file_exists($file_path)){
            throw new
Exception("File not exists ${file_path}");
        }
       
$text = fopen($file_path, 'r');
        while(
$line=fgets($text)){
            list(
$key, $param) = explode('=', $line);
            yield
$key => $param;
        }
    }
?>
//Usage : parse_ini('param.ini') // returns Generator Object
//Usage : iterator_to_array(parse_ini('param.ini')); // returns an array
up
-3
Anonymous
7 months ago
Same example, different results:

----------------------------------
           |  time  | memory, mb |
----------------------------------
| not gen  | 0.7589 | 146.75     |
|---------------------------------
| with gen | 0.7469 | 8.75       |
|---------------------------------

Time in results varying from 6.5 to 7.8 on both exassmples.
So no real drawbacks concerning processing speed.
To Top