PHP, C #, Python et probablement quelques autres langages ont un yield
mot-clé qui est utilisé pour créer des fonctions de générateur.
En PHP: http://php.net/manual/en/language.generators.syntax.php
En Python: https://www.pythoncentral.io/python-generators-and-yield-keyword/
En C #: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/yield
Je suis préoccupé par le fait qu'en tant que fonctionnalité / fonctionnalité de langue, cela yield
brise certaines conventions. L'un d'eux est ce à quoi je ferais référence, c'est la «certitude». Il s'agit d'une méthode qui renvoie un résultat différent chaque fois que vous l'appelez. Avec une fonction régulière non génératrice, vous pouvez l'appeler et si elle reçoit la même entrée, elle retournera la même sortie. Avec le rendement, il renvoie une sortie différente, en fonction de son état interne. Ainsi, si vous appelez au hasard la fonction de génération, sans connaître son état précédent, vous ne pouvez pas vous attendre à ce qu'elle renvoie un certain résultat.
Comment une fonction comme celle-ci s'intègre-t-elle dans le paradigme linguistique? Est-ce que cela rompt les conventions? Est-ce une bonne idée d'avoir et d'utiliser cette fonctionnalité? (pour donner un exemple de ce qui est bon et de ce qui était mauvais, goto
était autrefois une caractéristique de nombreux langages et l'est toujours, mais il est considéré comme nuisible et en tant que tel a été éradiqué de certains langages, tels que Java). Les compilateurs / interprètes de langage de programmation doivent-ils rompre les conventions pour implémenter une telle fonctionnalité, par exemple, un langage doit-il implémenter le multithread pour que cette fonctionnalité fonctionne, ou peut-il être fait sans technologie de thread?
yield
est essentiellement un moteur d'état. Ce n'est pas censé retourner le même résultat à chaque fois. Ce qu'il fera avec une certitude absolue, c'est retourner l'élément suivant dans un énumérable à chaque fois qu'il est invoqué. Les fils ne sont pas requis; vous avez besoin d'une fermeture (plus ou moins), afin de maintenir l'état actuel.yield
mot clé comme Python. Il a une méthode statiquestd::this_thread::yield()
, mais ce n'est pas un mot-clé. Ainsi, lethis_thread
serait précédé de presque tous les appels, ce qui rend assez évident qu'il s'agit d'une fonctionnalité de bibliothèque uniquement pour la génération de threads, et non d'une fonctionnalité de langage sur la génération de flux de contrôle en général.Réponses:
Avertissements en premier - C # est le langage que je connais le mieux, et bien qu'il en ait un
yield
qui semble être très similaire à d'autres langagesyield
, il peut y avoir des différences subtiles que je ne connais pas.Balivernes. Avez - vous vraiment attendre
Random.Next
ouConsole.ReadLine
de revenir le même résultat à chaque fois que vous les appelez? Et les appels Rest? Authentification? Supprimer l'article d'une collection? Il y a toutes sortes de fonctions (bonnes, utiles) impures.Oui,
yield
joue vraiment mal avectry/catch/finally
et est interdit ( https://blogs.msdn.microsoft.com/ericlippert/2009/07/16/iterator-blocks-part-three-why-no-yield-in-finally/ for Plus d'informations).C'est certainement une bonne idée d'avoir cette fonctionnalité. Des choses comme LINQ de C # sont vraiment agréables - l'évaluation paresseuse des collections offre un gros avantage en termes de performances, et
yield
permet de faire ce genre de chose dans une fraction du code avec une fraction des bogues qu'un itérateur roulé à la main le ferait.Cela dit, il n'y a pas une tonne d'utilisations en
yield
dehors du traitement de la collection de style LINQ. Je l'ai utilisé pour le traitement de validation, la génération de programme, la randomisation et quelques autres choses, mais je m'attends à ce que la plupart des développeurs ne l'aient jamais utilisé (ou mal utilisé).Pas exactement. Le compilateur génère un itérateur de machine d'état qui garde la trace de l'endroit où il s'est arrêté afin qu'il puisse recommencer la prochaine fois qu'il sera appelé. Le processus de génération de code s'apparente à Continuation Passing Style, où le code après
yield
est tiré dans son propre bloc (et s'il a desyield
s, un autre sous-bloc, etc.). C'est une approche bien connue utilisée plus souvent hors de la programmation fonctionnelle et qui apparaît également dans la compilation asynchrone / attente de C #.Aucun filetage n'est nécessaire, mais il nécessite une approche différente de la génération de code dans la plupart des compilateurs et présente un certain conflit avec d'autres fonctionnalités du langage.
Dans l'ensemble, cependant,
yield
c'est une fonctionnalité à impact relativement faible qui aide vraiment avec un sous-ensemble spécifique de problèmes.la source
yield
mot-clé est similaire aux coroutines, oui, ou quelque chose de différent? Si c'est le cas, j'aimerais en avoir un en C! Je peux penser à au moins quelques sections décentes de code qui auraient été tellement plus faciles à écrire avec une telle fonctionnalité de langage.async
/ aawait
été ajouté à la langue, quelqu'un l'a implémentée en utilisantyield
.J'aimerais répondre à cela dans une perspective Python avec un oui catégorique , c'est une excellente idée .
Je commencerai par aborder quelques questions et hypothèses dans votre question, puis démontrerai l'omniprésence des générateurs et leur utilité déraisonnable en Python plus tard.
C'est faux. Les méthodes sur les objets peuvent être considérées comme des fonctions elles-mêmes, avec leur propre état interne. En Python, puisque tout est un objet, vous pouvez réellement obtenir une méthode à partir d'un objet et passer autour de cette méthode (qui est liée à l'objet dont elle est issue, donc elle se souvient de son état).
D'autres exemples incluent des fonctions délibérément aléatoires ainsi que des méthodes d'entrée comme le réseau, le système de fichiers et le terminal.
Si le paradigme du langage prend en charge des éléments tels que les fonctions de première classe et que les générateurs prennent en charge d'autres fonctionnalités du langage comme le protocole Iterable, ils s'intègrent parfaitement.
Non. Puisqu'il est intégré dans le langage, les conventions sont construites autour et incluent (ou nécessitent!) L'utilisation de générateurs.
Comme pour toute autre fonctionnalité, le compilateur doit simplement être conçu pour prendre en charge la fonctionnalité. Dans le cas de Python, les fonctions sont déjà des objets avec état (tels que les arguments par défaut et les annotations de fonction).
Fait amusant: l'implémentation par défaut de Python ne prend pas du tout en charge le threading. Il dispose d'un verrou d'interpréteur global (GIL), donc rien ne s'exécute simultanément à moins que vous n'ayez lancé un deuxième processus pour exécuter une autre instance de Python.
note: les exemples sont en Python 3
Au-delà du rendement
Bien que le
yield
mot - clé puisse être utilisé dans n'importe quelle fonction pour le transformer en générateur, ce n'est pas le seul moyen d'en créer un. Python propose des expressions de générateur, un moyen puissant d'exprimer clairement un générateur en termes d'un autre itérable (y compris d'autres générateurs)Comme vous pouvez le voir, non seulement la syntaxe est claire et lisible, mais les fonctions intégrées comme
sum
acceptent les générateurs.Avec
Consultez la proposition d'amélioration Python pour l' instruction With . C'est très différent de ce que vous pourriez attendre d'une instruction With dans d'autres langues. Avec un peu d'aide de la bibliothèque standard, les générateurs de Python fonctionnent à merveille comme gestionnaires de contexte pour eux.
Bien sûr, imprimer des choses est la chose la plus ennuyeuse que vous puissiez faire ici, mais cela montre des résultats visibles. Les options les plus intéressantes incluent la gestion automatique des ressources (ouverture et fermeture de fichiers / flux / connexions réseau), le verrouillage pour l'accès simultané, l'habillage temporaire ou le remplacement d'une fonction, et la décompression puis la recompression des données. Si appeler des fonctions, c'est comme injecter du code dans votre code, alors avec des instructions, c'est comme encapsuler des parties de votre code dans un autre code. Quelle que soit la façon dont vous l'utilisez, c'est un exemple solide de connexion facile à une structure de langage. Les générateurs basés sur le rendement ne sont pas le seul moyen de créer des gestionnaires de contexte, mais ils sont certainement pratiques.
Pour et épuisement partiel
Pour que les boucles en Python fonctionnent de manière intéressante. Ils ont le format suivant:
Tout d'abord, l'expression que j'ai appelée
<iterable>
est évaluée pour obtenir un objet itérable. Deuxièmement, l'itérable l'a__iter__
appelé et l'itérateur résultant est stocké en arrière-plan. Par la suite,__next__
est appelé sur l'itérateur pour obtenir une valeur à lier au nom que vous entrez<name>
. Cette étape se répète jusqu'à ce que l'appel à__next__
lancer aStopIteration
. L'exception est avalée par la boucle for et l'exécution continue à partir de là.Revenons aux générateurs: lorsque vous faites appel
__iter__
à un générateur, il revient tout seul.Cela signifie que vous pouvez séparer l'itération sur quelque chose de la chose que vous voulez en faire, et changer ce comportement à mi-chemin. Ci-dessous, notez comment le même générateur est utilisé dans deux boucles, et dans la seconde, il commence à s'exécuter là où il s'était arrêté depuis la première.
Évaluation paresseuse
L'un des inconvénients des générateurs par rapport aux listes est que la seule chose à laquelle vous pouvez accéder dans un générateur est la prochaine chose qui en sort. Vous ne pouvez pas revenir en arrière et comme pour un résultat précédent, ou passer à un résultat ultérieur sans passer par les résultats intermédiaires. Le côté positif de ceci est qu'un générateur peut occuper presque aucune mémoire par rapport à sa liste équivalente.
Les générateurs peuvent également être enchaînés paresseusement.
Les première, deuxième et troisième lignes définissent simplement un générateur chacune, mais ne font aucun travail réel. Lorsque la dernière ligne est appelée, sum demande à numericcolumn une valeur, numericcolumn a besoin d'une valeur de lastcolumn, lastcolumn demande une valeur à partir du fichier journal, qui lit alors réellement une ligne du fichier. Cette pile se déroule jusqu'à ce que sum obtienne son premier entier. Ensuite, le processus se produit à nouveau pour la deuxième ligne. À ce stade, la somme a deux entiers et les additionne. Notez que la troisième ligne n'a pas encore été lue dans le fichier. Sum continue ensuite à demander des valeurs à numericcolumn (totalement inconscient du reste de la chaîne) et à les ajouter, jusqu'à ce que numericcolumn soit épuisé.
La partie vraiment intéressante ici est que les lignes sont lues, consommées et jetées individuellement. À aucun moment, le fichier entier n'est en mémoire à la fois. Que se passe-t-il si ce fichier journal est, disons, un téraoctet? Cela fonctionne, car il ne lit qu'une ligne à la fois.
Conclusion
Ce n'est pas une revue complète de toutes les utilisations des générateurs en Python. Notamment, j'ai sauté des générateurs infinis, des machines à états, en passant des valeurs et leur relation avec les coroutines.
Je crois que cela suffit pour démontrer que vous pouvez avoir des générateurs comme une fonctionnalité de langage utile parfaitement intégrée.
la source
Si vous êtes habitué aux langages OOP classiques, les générateurs et
yield
peuvent sembler dérangeants car l'état mutable est capturé au niveau de la fonction plutôt qu'au niveau de l'objet.La question de la «certitude» est cependant un problème. Elle est généralement appelée transparence référentielle et signifie essentiellement que la fonction renvoie toujours le même résultat pour les mêmes arguments. Dès que vous avez un état mutable, vous perdez la transparence référentielle. Dans la POO, les objets ont souvent un état mutable, ce qui signifie que le résultat de l'appel de méthode ne dépend pas seulement des arguments, mais aussi de l'état interne de l'objet.
La question est de savoir où capturer l'état mutable. Dans une POO classique, l'état mutable existe au niveau de l'objet. Mais si un langage prend en charge les fermetures, vous pouvez avoir un état mutable au niveau de la fonction. Par exemple en JavaScript:
En bref, il
yield
est naturel dans une langue fermeture de soutien, mais serait hors de propos dans une langue comme ancienne version de Java où l' état mutable n'existe au niveau de l' objet.la source
À mon avis, ce n'est pas une bonne caractéristique. C'est une mauvaise caractéristique, principalement parce qu'elle doit être enseignée très soigneusement et que tout le monde l'enseigne mal. Les gens utilisent le mot «générateur», équivoque entre la fonction de générateur et l'objet générateur. La question est: juste qui ou quoi fait le rendement réel?
Ce n'est pas simplement mon avis. Même Guido, dans le bulletin PEP dans lequel il se prononce, admet que la fonction générateur n'est pas un générateur mais une «usine de générateur».
C'est assez important, tu ne crois pas? Mais en lisant 99% de la documentation, vous auriez l'impression que la fonction de générateur est le générateur réel, et ils ont tendance à ignorer le fait que vous avez également besoin d'un objet générateur.
Guido a envisagé de remplacer «def» par «gen» pour ces fonctions et a dit non. Mais je dirais que cela n'aurait pas été suffisant de toute façon. Cela devrait vraiment être:
la source