les générateurs¶
rappels¶
pour fabriquer des itérables en Python
les types de base / containers (
list
,set
,dict
)compréhensions - construisent un container, et donc
allouent de la mémoire
font vraiment les calculs tout de suite
expression génératrice
syntaxe très voisine de la compréhension de liste
mais construit uniquement un itérateur
qui pourra - quand on l’utilisera - passer au suivant, et ainsi de suite
la fonction génératrice¶
la fonction génératrice est une dernière forme très commune d’itérateurs
écrite sous la forme d’une fonction
qui faityield
au lieu dereturn
# si une fonction contient
# au moins un yield
# elle devient une
# fonction génératrice
def squares(iterable):
for i in iterable:
yield i**2
data = (4, 1, 7)
# cet objet est un générateur
squares(data)
<generator object squares at 0x7f410af2acf0>
# en particulier
# on peut itérer dessus
for square in squares(data):
print(square, end=" ")
16 1 49
vocabulaire¶
une expression génératrice retourne un objet de type
generator
il est fréquent - par abus de langage - d’appeler aussi simplement générateur
une fonction génératricemais précisément, c’est l’appel à une fonction génératrice
qui retourne un objet de typegenerator
on a donc deux syntaxes différentes pour construire
des objets qui sont tous de typegenerator
expression génératrice vs fonction génératrice¶
# ces deux objets sont équivalents
gen1 = (x**2 for x in data)
def squares(iterable):
for i in iterable:
yield i**2
gen2 = squares(data)
for x in gen1:
print(x)
16
1
49
for x in gen2:
print(x)
16
1
49
expression génératrice vs fonction génératrice¶
les deux formes de générateur (expression et fonction)
produisent des objets de même typegenerator
# une genexpr
gen1 = (x**2 for x in data)
type(gen1)
generator
# (le résultat d'une)
# fonction génératrice
gen2 = squares(data)
type(gen2)
generator
la fonction a toutefois une puissance d’expression supérieure
notamment elle permet de conserver l’état de l’itération
sous la forme de variables locales
exercice¶
implémenter un générateur qui parcourt tous les nombres premiers
yield from
¶
une fonction génératrice est une fonction
donc elle peut appeler d’autres fonctions
qui peuvent elles-mêmes être des fonctions génératrices
exemple :
partant d’une fonction génératrice qui énumère
tous les diviseurs d’un entier (1 et lui-même exclus)
comment écrire un générateur
qui énumère tous les diviseurs
… des diviseurs de n
?
# on énumère les diviseurs
# en partant du plus grand
def divs(n):
for i in range(n-1, 1, -1):
if n % i == 0:
yield i
for div in divs(12):
print(div, end=" ")
6 4 3 2
# maintenant on voudrait écrire
# quelque chose qui fasse en gros
n = 12
for d1 in divs(n):
for d2 in divs(d1):
print(d2)
3
2
2
mais sous forme de fonction génératrice
yield from
¶
pour énumérer les diviseurs des diviseurs, on pourrait penser écrire
# première idée
# pourquoi ça ne marche pas ?
def divsdivs1(n):
for d in divs(n):
return divs(d)
# c'est bien un générateur
divsdivs1(12)
<generator object divs at 0x7f4108e1dac0>
# mais...
for d in divsdivs1(12):
print(d)
3
2
on entre dans le
for
avec d=6on évalue
divs(6)
(un générateur)et c’est ça qu’on retourne de suite
deuxième idée
pour énumérer les diviseurs des diviseurs, on pourrait penser écrire
# pourquoi ça ne marche pas ?
def divsdivs2(n):
for d in divs(n):
yield divs(d)
# c'est bien un générateur
divsdivs2(12)
<generator object divsdivs2 at 0x7f4108e1dc10>
# mais...
for d in divsdivs2(12):
print(d)
<generator object divs at 0x7f4108e1df90>
<generator object divs at 0x7f4108e1f040>
<generator object divs at 0x7f4108e1df90>
<generator object divs at 0x7f4108e1f040>
cette fois on va bien énumérer
divs(6)
,divs(4)
,divs(3)
puisdivs(2)
mais pas itérer sur ces 4 générateurs
c’est pourquoi ils se retrouvent imprimés tels quels
yield from
¶
on voit que lorsqu’une fonction génératrice en appelle une autre
il y a nécessité pour une syntaxe spéciale:
yield from
def divdivs(n):
for i in divs(n):
yield from divs(i)
for div in divdivs(12):
print(div)
3
2
2
itérateurs à base d’une classe¶
on peut aussi créer des objets qui sont des itérateurs, grâce à ce qu’on appelle le “protocole” itérable
dans les prochaines slides, on va construire de plusieurs façons différentes un objet qui a toujours les mêmes propriétés :
carres_n
est un itérateurqui est capable d’énumérer les carrés des nombres entiers
# en utilisant une fonction génératrice
def carres():
i = 0
while True:
yield i*i
i += 1
carres_1 = carres()
for _ in range(3):
print(next(carres_1))
0
1
4
# qu'on peut facilement remplacer par une expression génératrice
from itertools import count
carres_2 = (i**2 for i in count())
for _ in range(3):
print(next(carres_2))
0
1
4
# ou - un peu plus pédestre, dans ce cas d'usage
# on peut écrire une classe qui implémente
# __iter__ et __next__
#
# pour fabriquer un itérateur,
# il faut que __iter__() renvoie self
class Carres3:
def __init__(self):
self.i = 0
def __iter_(self):
return self
def __next__(self):
carre = self.i * self.i
self.i += 1
return carre
carres_3 = Carres3()
for _ in range(3):
print(next(carres_3))
0
1
4
# ici on crée un objet qui est bien itérable
# mais comme __iter__(self) renvoie autre chose que self,
# ce n'est pas un itérateur, on ne peut pas faire next() dessus
class Carres4:
def __iter__(self):
return ( i*i for i in count() )
carres_4 = Carres4()
# on ne peut pas faire next()
try:
for _ in range(3):
print(next(carres_4))
except Exception as exc:
print(f"OOPS, {type(exc)} {exc}")
OOPS, <class 'TypeError'> 'Carres4' object is not an iterator
# mais c'est quand même un itérable
# donc on peut faire un for avec
for x in carres_4:
print(x)
if x >= 4:
break
0
1
4