4.3 Générateurs
PHP a yield depuis PHP 5.5, donc le concept n'est pas nouveau pour toi. En Python c'est identique dans l'idĂ©e : une fonction avec yield retourne un gĂ©nĂ©rateur â un itĂ©rable paresseux qui produit les valeurs une par une, Ă la demande, sans tout charger en mĂ©moire. La diffĂ©rence clĂ© avec PHP : en Python, les gĂ©nĂ©rateurs sont omniprĂ©sents et au cĆur du langage (mĂȘme range() est un gĂ©nĂ©rateur).
1. yield vs return
return arrĂȘte la fonction et retourne une valeur. yield suspend la fonction, retourne une valeur, et reprend lĂ oĂč elle s'est arrĂȘtĂ©e au prochain appel.
def avec_return():
return [1, 2, 3] # construit toute la liste en mémoire, puis retourne
def avec_yield():
yield 1 # suspend ici, retourne 1
yield 2 # reprend ici au prochain next(), retourne 2
yield 3 # reprend ici, retourne 3
# la fonction se termine â StopIteration
# Les deux s'utilisent pareil dans un for
for n in avec_return(): print(n)
for n in avec_yield(): print(n) # mĂȘme rĂ©sultat, mais lazy
gen = avec_yield()
print(type(gen)) # <class 'generator'>
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
print(next(gen)) # StopIteration
- Un générateur n'exécute aucun code avant le premier
next() - En mémoire : seule la valeur courante existe, pas toute la séquence
forappellenext()automatiquement jusqu'ĂStopIteration
2. Generator expression
Comme les list comprehensions, mais avec des parenthĂšses â produit un gĂ©nĂ©rateur au lieu d'une liste. Utile quand on n'a besoin du rĂ©sultat qu'une seule fois.
nombres = [1, 2, 3, 4, 5, 6]
# List comprehension â construit toute la liste en mĂ©moire
carres_liste = [x ** 2 for x in nombres if x % 2 == 0]
# Generator expression â paresseux, mĂȘme syntaxe avec ()
carres_gen = (x ** 2 for x in nombres if x % 2 == 0)
print(list(carres_gen)) # [4, 16, 36]
print(sum(x**2 for x in range(1_000_000))) # pas de liste intermédiaire
3. yield from â dĂ©lĂ©guer Ă un autre itĂ©rable
yield from délÚgue l'itération à un autre générateur ou itérable, sans écrire la boucle manuellement. Utile pour combiner plusieurs sources.
def pip_lines(data: str):
for line in data.split('\n'):
yield line
def gomod_lines(data: str):
for line in data.split('\n'):
yield line
def toutes_les_lignes(pip_data: str, gomod_data: str):
yield from pip_lines(pip_data) # délÚgue à pip_lines
yield from gomod_lines(gomod_data) # puis Ă gomod_lines
for line in toutes_les_lignes(pip_raw, gomod_raw):
print(line)
4. Application projet : parser ligne par ligne
La version actuelle de parse_pip divise tout le fichier en liste avant de parcourir. Pour de gros fichiers (audit d'un serveur avec des centaines de paquets), on préfÚre un générateur interne qui yield un Component à la fois.
On sépare la logique en deux fonctions : le générateur brut _pip_components et la fonction publique parse_pip qui le consomme.
# parsers/pip.py
import re, logging
from models.component import Component
from utils.decorators import timer
def _pip_components(raw_data: str):
"""Générateur interne : yield un Component à la fois."""
for line in raw_data.splitlines():
line = line.strip()
if not line or "Package" in line or "---" in line:
continue
match = re.search(r"(\S+)\s+([\d\.]+)", line)
if match:
yield Component(name=match.group(1), version=match.group(2))
else:
logging.warning(f"Ligne ignorée : {line}")
@timer
def parse_pip(raw_data: str) -> list[Component]:
return list(_pip_components(raw_data))
L'avantage devient visible quand on traite un fichier directement â on peut filtrer ou limiter sans jamais construire la liste complĂšte :
# Traiter uniquement les 10 premiers composants
from itertools import islice
from parsers.pip import _pip_components
raw = open("scanner.txt").read()
premiers = list(islice(_pip_components(raw), 10))
# Chercher un composant spécifique sans tout charger
cible = next(
(c for c in _pip_components(raw) if c.name == "requests"),
None
)
[timer] parse_pip 0.0005s
--- Inventaire (3 items) ---
â requests == 2.28.1 â pkg:pypi/requests@2.28.1
â flask == 2.2.2 â pkg:pypi/flask@2.2.2
â cryptography == 38.0.1 â pkg:pypi/cryptography@38.0.1
â sbom.json gĂ©nĂ©rĂ©
La fonction publique parse_pip reste identique pour les appelants â seul le gĂ©nĂ©rateur interne change. C'est le principe : exposer une interface stable, optimiser l'implĂ©mentation.