Avoir une installation en langage générateur est-il yield
une 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 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)
>>> 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 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.
>>> 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.
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.