Python Language
Générateurs
Recherche…
Introduction
Les générateurs sont des itérateurs paresseux créés par des fonctions de générateur (à l'aide de yield
) ou des expressions de générateur (using (an_expression for x in an_iterator)
).
Syntaxe
- rendement
<expr>
- rendement de
<expr>
-
<var>
= rendement<expr>
- suivant (
<iter>
)
Itération
Un objet générateur prend en charge le protocole itérateur . En d'autres __next__()
, il fournit une méthode next()
( __next__()
dans Python 3.x), qui est utilisée pour __iter__
son exécution, et sa méthode __iter__
. Cela signifie qu'un générateur peut être utilisé dans n'importe quelle construction de langage prenant en charge les objets itérables génériques.
# naive partial implementation of the Python 2.x xrange()
def xrange(n):
i = 0
while i < n:
yield i
i += 1
# looping
for i in xrange(10):
print(i) # prints the values 0, 1, ..., 9
# unpacking
a, b, c = xrange(3) # 0, 1, 2
# building a list
l = list(xrange(10)) # [0, 1, ..., 9]
La fonction next ()
La fonction intégrée next()
est un wrapper pratique qui peut être utilisé pour recevoir une valeur de n'importe quel itérateur (y compris un générateur d'itérateurs) et pour fournir une valeur par défaut si l'itérateur est épuisé.
def nums():
yield 1
yield 2
yield 3
generator = nums()
next(generator, None) # 1
next(generator, None) # 2
next(generator, None) # 3
next(generator, None) # None
next(generator, None) # None
# ...
La syntaxe est la next(iterator[, default])
. Si l'itérateur se termine et qu'une valeur par défaut a été transmise, il est renvoyé. Si aucune valeur par défaut n'a été fournie, StopIteration
est StopIteration
.
Envoi d'objets à un générateur
En plus de recevoir des valeurs d'un générateur, il est possible d' envoyer un objet à un générateur à l'aide de la méthode send()
.
def accumulator():
total = 0
value = None
while True:
# receive sent value
value = yield total
if value is None: break
# aggregate values
total += value
generator = accumulator()
# advance until the first "yield"
next(generator) # 0
# from this point on, the generator aggregates values
generator.send(1) # 1
generator.send(10) # 11
generator.send(100) # 111
# ...
# Calling next(generator) is equivalent to calling generator.send(None)
next(generator) # StopIteration
Ce qui se passe ici est le suivant:
- Lorsque vous appelez
next(generator)
, le programme avance à la première déclaration deyield
et retourne la valeur detotal
à ce point, qui est 0. L'exécution du générateur est suspendue à ce stade. - Lorsque vous appelez ensuite
generator.send(x)
, l'interpréteur prend l'argumentx
et en fait la valeur de retour de la dernière instruction deyield
, qui est affectée à lavalue
. Le générateur se déroule alors comme d'habitude, jusqu'à ce qu'il produise la valeur suivante. - Lorsque vous appelez enfin
next(generator)
, le programme traite cela comme si vous envoyiezNone
au générateur. Il n'y a rien de particulier à propos deNone
, cependant, cet exemple utiliseNone
comme valeur spéciale pour demander au générateur de s'arrêter.
Expressions du générateur
Il est possible de créer des itérateurs de générateur en utilisant une syntaxe de type compréhension.
generator = (i * 2 for i in range(3))
next(generator) # 0
next(generator) # 2
next(generator) # 4
next(generator) # raises StopIteration
Si une fonction n'a pas nécessairement besoin d'une liste, vous pouvez enregistrer des caractères (et améliorer la lisibilité) en plaçant une expression de générateur dans un appel de fonction. La parenthèse de l'appel de fonction fait implicitement de votre expression une expression de générateur.
sum(i ** 2 for i in range(4)) # 0^2 + 1^2 + 2^2 + 3^2 = 0 + 1 + 4 + 9 = 14
De plus, vous économiserez de la mémoire car au lieu de charger la liste complète que vous parcourez ( [0, 1, 2, 3]
dans l'exemple ci-dessus), le générateur permet à Python d'utiliser les valeurs nécessaires.
introduction
Les expressions de générateur sont similaires à celles de liste, dictionnaire et ensemble, mais sont entourées de parenthèses. Les parenthèses ne doivent pas nécessairement être présentes lorsqu'elles sont utilisées comme seul argument pour un appel de fonction.
expression = (x**2 for x in range(10))
Cet exemple génère les 10 premiers carrés parfaits, dont 0 (dans lequel x = 0).
Les fonctions du générateur sont similaires aux fonctions normales, sauf qu’elles comportent un ou plusieurs énoncés de yield
. Ces fonctions ne peuvent return
aucune valeur (toutefois, les return
vides sont autorisés si vous souhaitez arrêter le générateur au début).
def function():
for x in range(10):
yield x**2
Cette fonction de générateur est équivalente à l'expression précédente du générateur, elle produit la même chose.
Note : toutes les expressions du générateur ont leurs propres fonctions équivalentes , mais pas l'inverse.
Une expression de générateur peut être utilisée sans parenthèses si les deux parenthèses sont répétées autrement:
sum(i for i in range(10) if i % 2 == 0) #Output: 20
any(x = 0 for x in foo) #Output: True or False depending on foo
type(a > b for a in foo if a % 2 == 1) #Output: <class 'generator'>
Au lieu de:
sum((i for i in range(10) if i % 2 == 0))
any((x = 0 for x in foo))
type((a > b for a in foo if a % 2 == 1))
Mais non:
fooFunction(i for i in range(10) if i % 2 == 0,foo,bar)
return x = 0 for x in foo
barFunction(baz, a > b for a in foo if a % 2 == 1)
L'appel d'une fonction de générateur produit un objet générateur , qui peut ensuite être itéré. Contrairement aux autres types d'itérateurs, les objets générateurs ne peuvent être parcourus qu'une seule fois.
g1 = function()
print(g1) # Out: <generator object function at 0x1012e1888>
Notez que le corps d'un générateur n'est pas exécuté immédiatement: lorsque vous appelez function()
dans l'exemple ci-dessus, il retourne immédiatement un objet générateur, sans même exécuter la première instruction d'impression. Cela permet aux générateurs de consommer moins de mémoire que les fonctions qui renvoient une liste et permet de créer des générateurs produisant des séquences infiniment longues.
Pour cette raison, les générateurs sont souvent utilisés dans la science des données et dans d'autres contextes impliquant de grandes quantités de données. Un autre avantage est que l'autre code peut immédiatement utiliser les valeurs fournies par un générateur, sans attendre que la séquence complète soit produite.
Cependant, si vous devez utiliser les valeurs produites par un générateur plusieurs fois et si leur génération coûte plus cher que le stockage, il peut être préférable de stocker les valeurs fournies sous forme de list
plutôt que de générer à nouveau la séquence. Voir 'Réinitialiser un générateur' ci-dessous pour plus de détails.
Un objet générateur est généralement utilisé dans une boucle ou dans toute fonction nécessitant une itération:
for x in g1:
print("Received", x)
# Output:
# Received 0
# Received 1
# Received 4
# Received 9
# Received 16
# Received 25
# Received 36
# Received 49
# Received 64
# Received 81
arr1 = list(g1)
# arr1 = [], because the loop above already consumed all the values.
g2 = function()
arr2 = list(g2) # arr2 = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Les objets générateurs étant des itérateurs, on peut les parcourir manuellement en utilisant la fonction next()
. Cela renverra les valeurs obtenues une par une à chaque invocation suivante.
Sous le capot, chaque fois que vous appelez next()
sur un générateur, Python exécute des instructions dans le corps de la fonction du générateur jusqu'à ce qu'il atteigne la déclaration de yield
suivante. À ce stade, il retourne l'argument de la commande de yield
et se souvient du point où cela s'est produit. L'appel next()
reprendra l'exécution à partir de ce point et continuera jusqu'à la déclaration de yield
suivante.
Si Python atteint la fin de la fonction du générateur sans plus de yield
s, une exception StopIteration
est StopIteration
(ceci est normal, tous les itérateurs se comportent de la même manière).
g3 = function()
a = next(g3) # a becomes 0
b = next(g3) # b becomes 1
c = next(g3) # c becomes 2
...
j = next(g3) # Raises StopIteration, j remains undefined
Notez que dans Python 2, les objets du générateur avaient des méthodes .next()
qui pouvaient être utilisées pour parcourir manuellement les valeurs générées. Dans Python 3, cette méthode a été remplacée par la norme .__next__()
pour tous les itérateurs.
Réinitialisation d'un générateur
Rappelez-vous que vous ne pouvez parcourir que les objets générés par un générateur une fois . Si vous avez déjà parcouru les objets dans un script, toute nouvelle tentative à cet effet aboutira à None
.
Si vous devez utiliser plusieurs fois les objets générés par un générateur, vous pouvez soit définir à nouveau la fonction du générateur et l'utiliser une seconde fois, ou vous pouvez également stocker la sortie de la fonction du générateur dans une liste lors de la première utilisation. La redéfinition de la fonction du générateur sera une bonne option si vous traitez de gros volumes de données et que le stockage d'une liste de tous les éléments de données prendrait beaucoup d'espace disque. À l'inverse, s'il est coûteux de générer les éléments initialement, vous pouvez préférer les stocker dans une liste pour pouvoir les réutiliser.
Utiliser un générateur pour trouver les numéros de Fibonacci
Un cas d'utilisation pratique d'un générateur consiste à parcourir les valeurs d'une série infinie. Voici un exemple de recherche des dix premiers termes de la séquence de Fibonacci .
def fib(a=0, b=1):
"""Generator that yields Fibonacci numbers. `a` and `b` are the seed values"""
while True:
yield a
a, b = b, a + b
f = fib()
print(', '.join(str(next(f)) for _ in range(10)))
0, 1, 1, 2, 3, 5, 8, 13, 21, 34
Séquences infinies
Les générateurs peuvent être utilisés pour représenter des séquences infinies:
def integers_starting_from(n):
while True:
yield n
n += 1
natural_numbers = integers_starting_from(1)
Une séquence infinie de nombres comme ci-dessus peut également être générée à l'aide de itertools.count
. Le code ci-dessus pourrait être écrit comme ci-dessous
natural_numbers = itertools.count(1)
Vous pouvez utiliser les compréhensions de générateur sur des générateurs infinis pour produire de nouveaux générateurs:
multiples_of_two = (x * 2 for x in natural_numbers)
multiples_of_three = (x for x in natural_numbers if x % 3 == 0)
Sachez qu’un générateur infini n’a pas de fin, donc le transmettre à une fonction qui essaiera de consommer le générateur entièrement aura des conséquences désastreuses :
list(multiples_of_two) # will never terminate, or raise an OS-specific error
Au lieu de cela, utilisez list / set compréhensions avec range
(ou xrange
pour python <3.0):
first_five_multiples_of_three = [next(multiples_of_three) for _ in range(5)]
# [3, 6, 9, 12, 15]
ou utilisez itertools.islice()
pour découper l'itérateur en un sous-ensemble:
from itertools import islice
multiples_of_four = (x * 4 for x in integers_starting_from(1))
first_five_multiples_of_four = list(islice(multiples_of_four, 5))
# [4, 8, 12, 16, 20]
Notez que le générateur d'origine est également mis à jour, comme tous les autres générateurs provenant de la même racine:
next(natural_numbers) # yields 16
next(multiples_of_two) # yields 34
next(multiples_of_four) # yields 24
Une séquence infinie peut également être itérée avec un for
-loop . Assurez-vous d'inclure une instruction de break
conditionnelle pour que la boucle se termine éventuellement:
for idx, number in enumerate(multiplies_of_two):
print(number)
if idx == 9:
break # stop after taking the first 10 multiplies of two
Exemple classique - Numéros de Fibonacci
import itertools
def fibonacci():
a, b = 1, 1
while True:
yield a
a, b = b, a + b
first_ten_fibs = list(itertools.islice(fibonacci(), 10))
# [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
def nth_fib(n):
return next(itertools.islice(fibonacci(), n - 1, n))
ninety_nineth_fib = nth_fib(99) # 354224848179261915075
Céder toutes les valeurs d'une autre itération
Utilisez le yield from
si vous souhaitez générer toutes les valeurs d'une autre itération:
def foob(x):
yield from range(x * 2)
yield from range(2)
list(foob(5)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1]
Cela fonctionne également avec des générateurs.
def fibto(n):
a, b = 1, 1
while True:
if a >= n: break
yield a
a, b = b, a + b
def usefib():
yield from fibto(10)
yield from fibto(20)
list(usefib()) # [1, 1, 2, 3, 5, 8, 1, 1, 2, 3, 5, 8, 13]
Coroutines
Les générateurs peuvent être utilisés pour implémenter des coroutines:
# create and advance generator to the first yield
def coroutine(func):
def start(*args,**kwargs):
cr = func(*args,**kwargs)
next(cr)
return cr
return start
# example coroutine
@coroutine
def adder(sum = 0):
while True:
x = yield sum
sum += x
# example use
s = adder()
s.send(1) # 1
s.send(2) # 3
Les coroutines sont couramment utilisées pour implémenter des machines à états, car elles sont principalement utiles pour créer des procédures à méthode unique nécessitant un état pour fonctionner correctement. Ils opèrent sur un état existant et renvoient la valeur obtenue à l'issue de l'opération.
Rendement avec récursivité: liste récursive de tous les fichiers d'un répertoire
Tout d'abord, importez les bibliothèques qui fonctionnent avec les fichiers:
from os import listdir
from os.path import isfile, join, exists
Une fonction d'assistance pour lire uniquement les fichiers d'un répertoire:
def get_files(path):
for file in listdir(path):
full_path = join(path, file)
if isfile(full_path):
if exists(full_path):
yield full_path
Une autre fonction d'assistance pour obtenir uniquement les sous-répertoires:
def get_directories(path):
for directory in listdir(path):
full_path = join(path, directory)
if not isfile(full_path):
if exists(full_path):
yield full_path
Maintenant, utilisez ces fonctions pour récupérer tous les fichiers dans un répertoire et tous ses sous-répertoires (en utilisant des générateurs):
def get_files_recursive(directory):
for file in get_files(directory):
yield file
for subdirectory in get_directories(directory):
for file in get_files_recursive(subdirectory): # here the recursive call
yield file
Cette fonction peut être simplifiée en utilisant le yield from
:
def get_files_recursive(directory):
yield from get_files(directory)
for subdirectory in get_directories(directory):
yield from get_files_recursive(subdirectory)
Itérer sur les générateurs en parallèle
Pour itérer plusieurs générateurs en parallèle, utilisez le zip
intégré:
for x, y in zip(a,b):
print(x,y)
Résulte en:
1 x
2 y
3 z
En python 2, utilisez plutôt itertools.izip
. Ici, nous pouvons également voir que toutes les fonctions zip
donnent des tuples.
Notez que zip cessera d’itérer dès que l’une des itérables sera à court d’éléments. Si vous souhaitez effectuer une itération aussi longue que la plus longue itération, utilisez itertools.zip_longest()
.
Code de construction de refactoring
Supposons que vous ayez un code complexe qui crée et renvoie une liste en commençant par une liste vide et en y ajoutant de manière répétée:
def create():
result = []
# logic here...
result.append(value) # possibly in several places
# more logic...
return result # possibly in several places
values = create()
Lorsqu'il n'est pas pratique de remplacer la logique interne par une compréhension de liste, vous pouvez transformer l'intégralité de la fonction en générateur, puis collecter les résultats:
def create_gen():
# logic...
yield value
# more logic
return # not needed if at the end of the function, of course
values = list(create_gen())
Si la logique est récursive, utilisez le yield from
pour inclure toutes les valeurs de l'appel récursif dans un résultat "aplati":
def preorder_traversal(node):
yield node.value
for child in node.children:
yield from preorder_traversal(child)
Recherche
La fonction next
est utile même sans itération. Passer une expression de générateur à next
est un moyen rapide de rechercher la première occurrence d'un élément correspondant à un prédicat. Code procédural comme
def find_and_transform(sequence, predicate, func):
for element in sequence:
if predicate(element):
return func(element)
raise ValueError
item = find_and_transform(my_sequence, my_predicate, my_func)
peut être remplacé par:
item = next(my_func(x) for x in my_sequence if my_predicate(x))
# StopIteration will be raised if there are no matches; this exception can
# be caught and transformed, if desired.
À cette fin, il peut être souhaitable de créer un alias, tel que first = next
, ou une fonction wrapper pour convertir l'exception:
def first(generator):
try:
return next(generator)
except StopIteration:
raise ValueError