Aller au contenu

Générateurs & itérateurs#

avancé

Protocole d'itération#

Un objet itérable implémente __iter__ (renvoie un itérateur). Un itérateur implémente __iter__ et __next__ (lève StopIteration en fin de séquence).

it = iter([1, 2, 3])
next(it)   # 1
next(it)   # 2
next(it)   # 3
next(it)   # StopIteration

Fonction génératrice (yield)#

def compteur(debut, fin):
    n = debut
    while n < fin:
        yield n
        n += 1

for x in compteur(0, 3):   # 0 1 2
    print(x)

g = compteur(0, 3)
next(g)   # 1er appel : exécute jusqu'au premier yield

Chaque appel à next() reprend l'exécution juste après le yield précédent (état conservé entre les appels).

Expression génératrice#

carres = (x**2 for x in range(1_000_000))   # ne construit rien en mémoire tant que non consommé

yield from#

def chaine(*iterables):
    for it in iterables:
        yield from it

list(chaine([1, 2], (3, 4), "ab"))   # [1, 2, 3, 4, 'a', 'b']

Pourquoi utiliser un générateur#

  • Traiter des séquences volumineuses (voire infinies) sans tout charger en mémoire.
  • Pipeline de transformations paresseuses (map, filter, générateurs enchaînés).
def lire(chemin):
    with open(chemin, encoding="utf-8") as f:   # le fichier reste ouvert le temps du parcours
        for ligne in f:
            yield ligne.strip()

non_vides = (l for l in lire("gros_fichier.txt") if l)
majuscules = (l.upper() for l in non_vides)     # rien n'est lu tant qu'on n'itère pas

Itérateur personnalisé (classe)#

class Compteur:
    def __init__(self, debut, fin):
        self.n = debut
        self.fin = fin

    def __iter__(self):
        return self

    def __next__(self):
        if self.n >= self.fin:
            raise StopIteration
        valeur = self.n
        self.n += 1
        return valeur

itertools (aperçu)#

from itertools import chain, islice, count, cycle, zip_longest, groupby, product, combinations

list(chain([1, 2], [3, 4]))                 # [1, 2, 3, 4] — concatène des itérables
list(islice(count(10), 3))                   # [10, 11, 12] — tranche d'un itérable infini
list(zip_longest([1, 2, 3], "ab", fillvalue=None))   # [(1,'a'), (2,'b'), (3,None)]
list(product([0, 1], repeat=2))                # [(0,0), (0,1), (1,0), (1,1)]
list(combinations("abc", 2))                    # [('a','b'), ('a','c'), ('b','c')]

# groupby : regroupe les éléments CONSÉCUTIFS égaux -> trier d'abord sur la même clé
donnees = sorted([("A", 1), ("B", 2), ("A", 3)], key=lambda t: t[0])
{cle: [v for _, v in grp] for cle, grp in groupby(donnees, key=lambda t: t[0])}
# {'A': [1, 3], 'B': [2]}

Piège

Un itérateur (générateur, map, zip, filter) ne se parcourt qu'une seule fois. Un second for sur le même objet ne produit rien.

g = (x**2 for x in range(3))
list(g)   # [0, 1, 4]
list(g)   # [] — générateur épuisé