Est-ce une bonne idée d'utiliser un langage de générateur tel que «yield»?

9

PHP, C #, Python et probablement quelques autres langages ont un yieldmot-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 yieldbrise 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?

Dennis
la source
4
yieldest 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.
Robert Harvey
1
En ce qui concerne la qualité de la «certitude», considérez que, étant donné la même séquence d'entrée, une série d'appels à l'itérateur donnera exactement les mêmes éléments dans exactement le même ordre.
Robert Harvey
4
Je ne sais pas d'où viennent la plupart de vos questions, car C ++ n'a pas de yield mot clé comme Python. Il a une méthode statique std::this_thread::yield(), mais ce n'est pas un mot-clé. Ainsi, le this_threadserait 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.
Ixrec
lien mis à jour en C #, un pour C ++ supprimé
Dennis

Réponses:

16

Avertissements en premier - C # est le langage que je connais le mieux, et bien qu'il en ait un yieldqui semble être très similaire à d'autres langages yield, il peut y avoir des différences subtiles que je ne connais pas.

Je crains qu'en tant que fonctionnalité / fonctionnalité de langue, le rendement ne respecte 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.

Balivernes. Avez - vous vraiment attendre Random.Nextou Console.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.

Comment une fonction comme celle-ci s'intègre-t-elle dans le paradigme linguistique? Est-ce que cela rompt les conventions?

Oui, yieldjoue vraiment mal avec try/catch/finallyet est interdit ( https://blogs.msdn.microsoft.com/ericlippert/2009/07/16/iterator-blocks-part-three-why-no-yield-in-finally/ for Plus d'informations).

Est-ce une bonne idée d'avoir et d'utiliser cette fonctionnalité?

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 yieldpermet 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 yielddehors 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é).

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?

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 yieldest tiré dans son propre bloc (et s'il a des yields, 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, yieldc'est une fonctionnalité à impact relativement faible qui aide vraiment avec un sous-ensemble spécifique de problèmes.

Telastyn
la source
Je n'ai jamais utilisé C # sérieusement mais ce yieldmot-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.
2
@DrunkCoder - similaire, mais avec certaines limitations, si je comprends bien.
Telastyn
1
Vous ne voudriez pas non plus voir le rendement mal utilisé. Plus une langue possède de fonctionnalités, plus vous trouverez probablement un programme mal écrit dans cette langue. Je ne sais pas si la bonne approche pour écrire une langue accessible est de tout jeter sur vous et de voir ce qui colle.
Neil
1
@DrunkCoder: c'est une version limitée des semi-coroutines. En fait, il est traité comme un modèle syntaxique par le compilateur qui est développé en une série d'appels de méthodes, de classes et d'objets. (Fondamentalement, le compilateur génère un objet de continuation qui capture le contexte actuel dans les champs.) L' implémentation par défaut pour les collections est une semi-coroutine, mais en surchargeant les méthodes "magiques" utilisées par le compilateur, vous pouvez réellement personnaliser le comportement. Par exemple, avant async/ a awaitété ajouté à la langue, quelqu'un l'a implémentée en utilisant yield.
Jörg W Mittag
1
@Neil Il est généralement possible de mal utiliser pratiquement toutes les fonctionnalités du langage de programmation. Si ce que vous dites était vrai, il serait beaucoup plus difficile de mal programmer en utilisant C que Python ou C #, mais ce n'est pas le cas car ces langages ont beaucoup d'outils qui protègent les programmeurs de nombreuses erreurs qui sont très faciles à faire avec C. En réalité, la cause des mauvais programmes est de mauvais programmeurs - c'est tout à fait un problème indépendant du langage.
Ben Cottrell
12

Avoir une installation en langage générateur est-il yieldune bonne idée?

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.

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.

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.

Comment une fonction comme celle-ci s'intègre-t-elle dans le paradigme linguistique?

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.

Est-ce que cela rompt les conventions?

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.

Les compilateurs / interprètes de langage de programmation doivent-ils rompre toute convention pour implémenter une telle fonctionnalité

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).

un langage doit-il implémenter le multi-thread pour que cette fonctionnalité fonctionne, ou peut-il être fait sans technologie de thread?

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 yieldmot - 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)

>>> pairs = ((x,y) for x in range(10) for y in range(10) if y >= x)
>>> pairs
<generator object <genexpr> at 0x0311DC90>
>>> sum(x*y for x,y in pairs)
1155

Comme vous pouvez le voir, non seulement la syntaxe est claire et lisible, mais les fonctions intégrées comme sumacceptent 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.

>>> from contextlib import contextmanager
>>> @contextmanager
def debugWith(arg):
        print("preprocessing", arg)
        yield arg
        print("postprocessing", arg)


>>> with debugWith("foobar") as s:
        print(s[::-1])


preprocessing foobar
raboof
postprocessing foobar

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:

for <name> in <iterable>:
    ...

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 a StopIteration. 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.

>>> x = (a for a in "boring generator")
>>> id(x)
51502272
>>> id(x.__iter__())
51502272

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.

>>> generator = (x for x in 'more boring stuff')
>>> for letter in generator:
        print(ord(letter))
        if letter > 'p':
                break


109
111
114
>>> for letter in generator:
        print(letter)


e

b
o
r
i
n
g

s
t
u
f
f

É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.

>>> import sys
>>> sys.getsizeof([x for x in range(10000)])
43816
>>> sys.getsizeof(range(10000000000))
24
>>> sys.getsizeof([x for x in range(10000000000)])
Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    sys.getsizeof([x for x in range(10000000000)])
  File "<pyshell#10>", line 1, in <listcomp>
    sys.getsizeof([x for x in range(10000000000)])
MemoryError

Les générateurs peuvent également être enchaînés paresseusement.

logfile = open("logs.txt")
lastcolumn = (line.split()[-1] for line in logfile)
numericcolumn = (float(x) for x in lastcolumn)
print(sum(numericcolumn))

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.

Joel Harmon
la source
6

Si vous êtes habitué aux langages OOP classiques, les générateurs et yieldpeuvent 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 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:

function getCounter() {
   var cnt = 1;
   return function(){ return cnt++; }
}
var counter = getCounter();
counter() --> 1
counter() --> 2

En bref, il yieldest 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.

JacquesB
la source
Je suppose que si les caractéristiques linguistiques avaient un spectre, le rendement serait aussi éloigné que possible de la fonctionnalité. Ce n'est pas nécessairement une mauvaise chose. La programmation orientée objet était autrefois très à la mode, et encore plus tard, une programmation fonctionnelle. Je suppose que le danger de cela revient vraiment à mélanger et à faire correspondre des fonctionnalités comme le rendement avec une conception fonctionnelle qui fait que votre programme se comporte de manière inattendue.
Neil
0

À 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:

def make_gen(args)
    def_gen foo
        # Put in "yield" and other beahvior
    return_gen foo
user320927
la source