Licence CC BY-NC-ND Thierry Parmentelat

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 fait yield au lieu de return

# 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ératrice

  • mais précisément, c’est l’appel à une fonction génératrice
    qui retourne un objet de type generator

  • on a donc deux syntaxes différentes pour construire
    des objets qui sont tous de type generator

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 type generator

# 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=6

  • on é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) puis divs(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érateur

  • qui 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