Résumé sur les générateurs

(PHP 5 >= 5.5.0, PHP 7, PHP 8)

Les générateurs fournissent une façon simple de mettre en place des itérateurs sans le coût ni la complexité du développement d'une classe qui implémente l'interface Iterator.

Un gĂ©nĂ©rateur offre un moyen pratique de fournir des donnĂ©es aux boucles foreach sans avoir Ă  construire un tableau en mĂ©moire Ă  l'avance, ce qui pourrait amener le programme Ă  dĂ©passer une limite de mĂ©moire ou nĂ©cessiter un temps de traitement considĂ©rable pour les gĂ©nĂ©rer. À la place, une fonction gĂ©nĂ©rateur peut ĂȘtre utilisĂ©e, qui est identique Ă  une fonction normale, mis Ă  part le fait qu'au lieu de retourner une seule fois, un gĂ©nĂ©rateur peut utiliser yield autant de fois que nĂ©cessaire, afin de fournir les valeurs Ă  parcourir. Comme avec les itĂ©rateurs, l'accĂšs alĂ©atoire aux donnĂ©es n'est pas possible.

Un exemple simple de ce mécanisme est la ré-implémentation de la fonction range() sous la forme d'un générateur. La fonction standard range() doit générer un tableau contenant chaque valeur, et le retourner, ce qui peut conduire à des tableaux de taille importante : par exemple, l'appel du code range(0, 1000000) peut consommer nettement plus de 100 Mo de mémoire.

Comme alternative, nous pouvons implémenter un générateur xrange(), qui n'aura comme besoin mémoire que la seule création d'un objet Iterator, et devra garder trace en interne du statut courant du générateur, ce qui revient à une consommation mémoire inférieure à 1 Ko.

Exemple #1 Implémentation de la fonction range() sous la forme d'un générateur

<?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;
}
}
}

/*
* Il est Ă  noter que les fonctions range() et xrange() produisent le
* mĂȘme affichage, ci-dessous.
*/

echo 'Nombres impairs Ă  un seul chiffre depuis range() : ';
foreach (
range(1, 9, 2) as $number) {
echo
"$number ";
}
echo
"\n";

echo
'Nombres impairs Ă  un seul chiffre depuis xrange() : ';
foreach (
xrange(1, 9, 2) as $number) {
echo
"$number ";
}

L'exemple ci-dessus va afficher :

Nombres impairs Ă  un seul chiffre depuis range() :  1 3 5 7 9
Nombres impairs Ă  un seul chiffre depuis xrange() : 1 3 5 7 9

Les objets Generator

Lorsqu'une fonction gĂ©nĂ©rateur est appelĂ©e, un objet de la classe interne Generator est retournĂ©. Cet objet implĂ©mente l'interface Iterator de la mĂȘme façon qu'un objet itĂ©rateur, qui avance uniquement, le ferait, et fournit les mĂ©thodes qui peuvent ĂȘtre appelĂ©es pour manipuler le statut du gĂ©nĂ©rateur, y compris l'envoi des valeurs et leurs retours.

add a note

User Contributed Notes 6 notes

up
180
bloodjazman at gmail dot com ¶
12 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
44
montoriusz at gmail dot com ¶
10 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
18
chung1905 at gmail dot com ¶
6 years 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
30
info at boukeversteegh dot nl ¶
10 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
17
dc at libertyskull dot com ¶
12 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
18
lubaev ¶
12 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%      |
----------------------------------