ジェネレータとは
(PHP 5 >= 5.5.0, PHP 7, PHP 8)
ジェネレータを使えば、シンプルな イテレータを簡単に実装できます。 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 positive');
}
for ($i = $start; $i <= $limit; $i += $step) {
yield $i;
}
} else {
if ($step >= 0) {
throw new LogicException('Step must be negative');
}
for ($i = $start; $i >= $limit; $i += $step) {
yield $i;
}
}
}
/*
* 次の例で、range() と xrange() が同じ結果を返すことに注目しましょう
*/
echo 'Single digit odd numbers from range(): ';
foreach (range(1, 9, 2) as $number) {
echo "$number ";
}
echo "\n";
echo 'Single digit odd numbers from xrange(): ';
foreach (xrange(1, 9, 2) 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
User Contributed Notes 6 notes
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;
}
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();
}
?>
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 **/
?>
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
}
?>
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% |
----------------------------------
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.