Que signifie rendement en PHP?

232

J'ai récemment trébuché sur ce code:

function xrange($min, $max) 
{
    for ($i = $min; $i <= $max; $i++) {
        yield $i;
    }
}

Je n'ai jamais vu ce yieldmot - clé auparavant. Essayer d'exécuter le code que j'obtiens

Erreur d'analyse: erreur de syntaxe, T_VARIABLE inattendu sur la ligne x

Alors, quel est ce yieldmot-clé? Est-ce même du PHP valide? Et si c'est le cas, comment l'utiliser?

Gordon
la source

Réponses:

355

Qu'est-ce que c'est yield?

Le yieldmot-clé renvoie les données d'une fonction de générateur:

Le cœur d'une fonction de générateur est le mot-clé yield. Dans sa forme la plus simple, une instruction yield ressemble beaucoup à une instruction return, sauf qu'au lieu d'arrêter l'exécution de la fonction et de revenir, yield fournit à la place une valeur au code en boucle sur le générateur et suspend l'exécution de la fonction du générateur.

Qu'est-ce qu'une fonction générateur?

Une fonction de générateur est en fait un moyen plus compact et plus efficace d'écrire un itérateur . Il vous permet de définir une fonction (votre xrange) qui calculera et renverra des valeurs pendant que vous la parcourez :

foreach (xrange(1, 10) as $key => $value) {
    echo "$key => $value", PHP_EOL;
}

Cela créerait la sortie suivante:

0 => 1
1 => 2

9 => 10

Vous pouvez également contrôler le $keydans le foreachen utilisant

yield $someKey => $someValue;

Dans la fonction de générateur, $someKeyest ce que vous voulez voir apparaître $keyet $someValueêtre la valeur $val. Dans l'exemple de la question, c'est $i.

Quelle est la différence avec les fonctions normales?

Maintenant, vous vous demandez peut-être pourquoi nous n'utilisons pas simplement la rangefonction native de PHP pour obtenir cette sortie. Et vous avez raison. La sortie serait la même. La différence, c'est comment nous y sommes arrivés.

Lorsque nous utilisons rangePHP, l'exécutons, créent le tableau entier de nombres en mémoire et returnce tableau entier dans la foreachboucle qui va ensuite le parcourir et sortir les valeurs. En d'autres termes, le foreachva fonctionner sur la baie de disques elle-même. La rangefonction et le foreachseul "parler" une fois. Pensez-y comme recevoir un colis par la poste. Le livreur vous remettra le colis et partira. Et puis vous déballez l'ensemble complet, en retirant tout ce qui s'y trouve.

Lorsque nous utilisons la fonction de générateur, PHP entrera dans la fonction et l'exécutera jusqu'à ce qu'elle rencontre la fin ou un yieldmot clé. Quand il rencontre un yield, il retournera alors quelle que soit la valeur à ce moment à la boucle externe. Ensuite, il revient dans la fonction de générateur et continue d'où il a cédé. Puisque votre xrangedétient une forboucle, il s'exécutera et cédera jusqu'à ce qu'il $maxsoit atteint. Pensez-y comme le foreachet le générateur jouant au ping-pong.

Pourquoi ai-je besoin de ça?

De toute évidence, les générateurs peuvent être utilisés pour contourner les limites de mémoire. En fonction de votre environnement, faire un range(1, 1000000)sera fatal à votre script alors que la même chose avec un générateur fonctionnera très bien. Ou, comme le dit Wikipedia:

Étant donné que les générateurs calculent leurs valeurs produites uniquement sur demande, ils sont utiles pour représenter des séquences qui seraient coûteuses ou impossibles à calculer à la fois. Il s'agit par exemple de séquences infinies et de flux de données en direct.

Les générateurs sont également censés être assez rapides. Mais gardez à l'esprit que lorsque nous parlons de rapidité, nous parlons généralement en très petit nombre. Donc, avant de vous enfuir et de modifier tout votre code pour utiliser des générateurs, faites un test pour voir où cela a du sens.

Un autre cas d'utilisation pour les générateurs est les coroutines asynchrones. Le yieldmot-clé ne renvoie pas seulement des valeurs mais il les accepte également. Pour plus de détails à ce sujet, consultez les deux excellents articles de blog liés ci-dessous.

Depuis quand puis-je utiliser yield?

Des générateurs ont été introduits en PHP 5.5 . Essayer d'utiliser yieldavant cette version entraînera diverses erreurs d'analyse, selon le code qui suit le mot clé. Donc, si vous obtenez une erreur d'analyse de ce code, mettez à jour votre PHP.

Sources et lectures complémentaires:

Gordon
la source
1
Veuillez préciser quels sont les avantages de yeild, disons, une solution comme celle-ci: ideone.com/xgqevM
Mike
1
Ah, eh bien, et les avis que je produisais. Huh. Eh bien, j'ai expérimenté l'émulation de générateurs pour PHP> = 5.0.0 avec une classe d'assistance, et oui, un peu moins lisible, mais je pourrai l'utiliser à l'avenir. Sujet intéressant. Merci!
Mike
Pas de lisibilité mais d'utilisation de la mémoire! Comparez la mémoire utilisée pour l'itération sur return range(1,100000000)et for ($i=0; $i<100000000; $i++) yield $i
Emix
@mike oui, c'est déjà expliqué dans ma réponse. Dans l'autre exemple, la mémoire de Mike n'est guère un problème car il ne fait qu'itérer 10 valeurs.
Gordon
1
@Mike Un problème avec le xrange est que son utilisation des limites statiques est utile pour l'imbrication par exemple (par exemple, la recherche sur une variété à n dimensions, ou un tri rapide récursif utilisant des générateurs, par exemple). Vous ne pouvez pas imbriquer des boucles xrange car il n'y a qu'une seule instance de son compteur. La version Yield ne souffre pas de ce problème.
Shayne
43

Cette fonction utilise le rendement:

function a($items) {
    foreach ($items as $item) {
        yield $item + 1;
    }
}

est presque le même que celui-ci sans:

function b($items) {
    $result = [];
    foreach ($items as $item) {
        $result[] = $item + 1;
    }
    return $result;
}

La seule différence est qu'il a()renvoie un générateur et b()juste un simple tableau. Vous pouvez parcourir les deux.

De plus, le premier n'alloue pas un tableau complet et est donc moins gourmand en mémoire.

tsusanka
la source
2
notes addt des documents officiels: En PHP 5, un générateur ne pouvait pas retourner une valeur: cela entraînerait une erreur de compilation. Une instruction de retour vide était une syntaxe valide dans un générateur et elle mettrait fin au générateur. Depuis PHP 7.0, un générateur peut renvoyer des valeurs, qui peuvent être récupérées à l'aide de Generator :: getReturn (). php.net/manual/en/language.generators.syntax.php
Programmeur Dancuk
Simple et concis.
John Miller
24

exemple simple

<?php
echo '#start main# ';
function a(){
    echo '{start[';
    for($i=1; $i<=9; $i++)
        yield $i;
    echo ']end} ';
}
foreach(a() as $v)
    echo $v.',';
echo '#end main#';
?>

production

#start main# {start[1,2,3,4,5,6,7,8,9,]end} #end main#

exemple avancé

<?php
echo '#start main# ';
function a(){
    echo '{start[';
    for($i=1; $i<=9; $i++)
        yield $i;
    echo ']end} ';
}
foreach(a() as $k => $v){
    if($k === 5)
        break;
    echo $k.'=>'.$v.',';
}
echo '#end main#';
?>

production

#start main# {start[0=>1,1=>2,2=>3,3=>4,4=>5,#end main#
Voir grand
la source
Alors, ça revient sans interrompre la fonction?
Lucas Bustamante
22

yieldLe mot-clé sert à la définition des "générateurs" en PHP 5.5. Ok, alors qu'est-ce qu'un générateur ?

De php.net:

Les générateurs fournissent un moyen facile d'implémenter des itérateurs simples sans la surcharge ou la complexité de l'implémentation d'une classe qui implémente l'interface Iterator.

Un générateur vous permet d'écrire du code qui utilise foreach pour parcourir un ensemble de données sans avoir besoin de créer un tableau en mémoire, ce qui peut vous faire dépasser une limite de mémoire ou nécessiter un temps de traitement considérable pour générer. Au lieu de cela, vous pouvez écrire une fonction de générateur, qui est identique à une fonction normale, sauf qu'au lieu de retourner une fois, un générateur peut produire autant de fois qu'il le faut afin de fournir les valeurs à itérer.

De cet endroit: générateurs = générateurs, autres fonctions (juste une simple fonction) = fonctions.

Ils sont donc utiles lorsque:

  • vous devez faire des choses simples (ou des choses simples);

    générateur est vraiment beaucoup plus simple que d'implémenter l'interface Iterator. d'autre part, il est évident que les générateurs sont moins fonctionnels. les comparer .

  • vous devez générer de GRANDES quantités de données pour économiser de la mémoire;

    en fait, pour économiser de la mémoire, nous pouvons simplement générer les données nécessaires via des fonctions pour chaque itération de boucle, et après l'itération, utiliser les ordures. voici donc les points principaux: un code clair et probablement des performances. voyez ce qui est mieux pour vos besoins.

  • vous devez générer une séquence qui dépend de valeurs intermédiaires;

    cela prolonge la pensée précédente. les générateurs peuvent faciliter les choses par rapport aux fonctions. vérifiez l' exemple de Fibonacci , et essayez de faire une séquence sans générateur. Les générateurs peuvent également fonctionner plus rapidement dans ce cas, du moins en raison du stockage des valeurs intermédiaires dans des variables locales;

  • vous devez améliorer les performances.

    ils peuvent fonctionner plus rapidement que les fonctions dans certains cas (voir avantage précédent);

QArea
la source
1
Je ne comprenais pas comment fonctionnaient les générateurs. cette classe implémente l'interface itérateur. d'après ce que je sais, les classes d'itérateurs me permettent de configurer la façon dont je veux itérer sur un objet. par exemple, ArrayIterator obtient un tableau ou un objet afin que je puisse modifier les valeurs et les clés tout en l'itérant. donc si les itérateurs obtiennent l'intégralité de l'objet / tableau alors comment le générateur n'a-t-il pas à construire le tableau entier dans la mémoire ???
user3021621
7

Avec, yieldvous pouvez facilement décrire les points d'arrêt entre plusieurs tâches dans une seule fonction. C'est tout, il n'y a rien de spécial à ce sujet.

$closure = function ($injected1, $injected2, ...){
    $returned = array();
    //task1 on $injected1
    $returned[] = $returned1;
//I need a breakpoint here!!!!!!!!!!!!!!!!!!!!!!!!!
    //task2 on $injected2
    $returned[] = $returned2;
    //...
    return $returned;
};
$returned = $closure($injected1, $injected2, ...);

Si task1 et task2 sont fortement liés, mais vous avez besoin d'un point d'arrêt entre eux pour faire autre chose:

  • mémoire libre entre les lignes de traitement de la base de données
  • exécuter d'autres tâches qui fournissent une dépendance à la tâche suivante, mais qui ne sont pas liées à la compréhension du code actuel
  • faire des appels asynchrones et attendre les résultats
  • etc ...

alors les générateurs sont la meilleure solution, car vous n'avez pas à diviser votre code en plusieurs fermetures ou à le mélanger avec un autre code, ou à utiliser des rappels, etc ... Vous utilisez simplement yieldpour ajouter un point d'arrêt, et vous pouvez continuer à partir de là point d'arrêt si vous êtes prêt.

Ajouter un point d'arrêt sans générateurs:

$closure1 = function ($injected1){
    //task1 on $injected1
    return $returned1;
};
$closure2 = function ($injected2){
    //task2 on $injected2
    return $returned1;
};
//...
$returned1 = $closure1($injected1);
//breakpoint between task1 and task2
$returned2 = $closure2($injected2);
//...

Ajouter un point d'arrêt avec des générateurs

$closure = function (){
    $injected1 = yield;
    //task1 on $injected1
    $injected2 = (yield($returned1));
    //task2 on $injected2
    $injected3 = (yield($returned2));
    //...
    yield($returnedN);
};
$generator = $closure();
$returned1 = $generator->send($injected1);
//breakpoint between task1 and task2
$returned2 = $generator->send($injected2);
//...
$returnedN = $generator->send($injectedN);

note: Il est facile de se tromper avec les générateurs, alors écrivez toujours des tests unitaires avant de les implémenter! note2: Utiliser des générateurs dans une boucle infinie, c'est comme écrire une fermeture de longueur infinie ...

inf3rno
la source
4

Aucune des réponses ci-dessus ne montre un exemple concret utilisant des tableaux massifs peuplés de membres non numériques. Voici un exemple utilisant un tableau généré par explode()sur un gros fichier .txt (262 Mo dans mon cas d'utilisation):

<?php

ini_set('memory_limit','1000M');

echo "Starting memory usage: " . memory_get_usage() . "<br>";

$path = './file.txt';
$content = file_get_contents($path);

foreach(explode("\n", $content) as $ex) {
    $ex = trim($ex);
}

echo "Final memory usage: " . memory_get_usage();

Le résultat était:

Starting memory usage: 415160
Final memory usage: 270948256

Comparez maintenant cela à un script similaire, en utilisant le yieldmot - clé:

<?php

ini_set('memory_limit','1000M');

echo "Starting memory usage: " . memory_get_usage() . "<br>";

function x() {
    $path = './file.txt';
    $content = file_get_contents($path);
    foreach(explode("\n", $content) as $x) {
        yield $x;
    }
}

foreach(x() as $ex) {
    $ex = trim($ex);
}

echo "Final memory usage: " . memory_get_usage();

La sortie de ce script était:

Starting memory usage: 415152
Final memory usage: 415616

De toute évidence, les économies d’utilisation de la mémoire ont été considérables (ΔMemoryUsage -----> ~ 270,5 Mo dans le premier exemple, ~ 450B dans le deuxième exemple).

David Partyka
la source
3

Un aspect intéressant, qui mérite d'être discuté ici, cède par référence . Chaque fois que nous devons changer un paramètre de sorte qu'il se reflète à l'extérieur de la fonction, nous devons passer ce paramètre par référence. Pour appliquer cela aux générateurs, nous ajoutons simplement une esperluette &au nom du générateur et à la variable utilisée dans l'itération:

 <?php 
 /**
 * Yields by reference.
 * @param int $from
 */
function &counter($from) {
    while ($from > 0) {
        yield $from;
    }
}

foreach (counter(100) as &$value) {
    $value--;
    echo $value . '...';
}

// Output: 99...98...97...96...95...

L'exemple ci-dessus montre comment la modification des valeurs itérées dans la foreachboucle modifie la $fromvariable dans le générateur. Cela est dû au fait qu'il $fromest fourni par référence en raison de l'esperluette avant le nom du générateur. Pour cette raison, la $valuevariable dans la foreachboucle est une référence à la $fromvariable dans la fonction de générateur.

Bud Damyanov
la source
0

Le code ci-dessous illustre comment l'utilisation d'un générateur renvoie un résultat avant la fin, contrairement à l'approche non génératrice traditionnelle qui renvoie un tableau complet après une itération complète. Avec le générateur ci-dessous, les valeurs sont retournées lorsqu'elles sont prêtes, pas besoin d'attendre qu'un tableau soit complètement rempli:

<?php 

function sleepiterate($length) {
    for ($i=0; $i < $length; $i++) {
        sleep(2);
        yield $i;
    }
}

foreach (sleepiterate(5) as $i) {
    echo $i, PHP_EOL;
}
Risteard
la source
Donc, il n'est pas possible d'utiliser yield pour générer du code html en php? Je ne connais pas les avantages dans un environnement réel
Giuseppe Lodi Rizzini
@GiuseppeLodiRizzini qu'est-ce qui vous fait penser cela?
Brad Kent