4.4 L'ùme de Python face à PHP : Flux, Décorateurs et Générateurs
On a vu les mĂ©canismes â yield, @decorator. Avant de passer Ă la CLI, une pause pour comprendre pourquoi ces outils existent et ce qu'ils rĂ©vĂšlent de la philosophie du langage. Comprendre l'Ăąme d'un langage, c'est arrĂȘter de traduire et commencer Ă penser dans ce langage.
1. Deux langages, deux raisons d'ĂȘtre
PHP et Python sont deux bons langages â mais ils n'ont pas Ă©tĂ© pensĂ©s pour le mĂȘme monde. Ce qui diffĂšre, ce n'est pas tant ce qu'ils peuvent faire que ce pourquoi ils ont Ă©tĂ© optimisĂ©s.
PHP est nĂ© pour le Web. Son ADN historique est le cycle Request â Traitement â Response : une requĂȘte arrive, on construit une rĂ©ponse, tout meurt. Le script s'exĂ©cute de A Ă Z, vite, puis la mĂ©moire est libĂ©rĂ©e. C'est un modĂšle en blocs : on charge, on traite, on retourne. Aujourd'hui PHP a beaucoup Ă©voluĂ© â il peut tourner en CLI, en workers long-lived avec RoadRunner ou Swoole, et mĂȘme faire du streaming avec SplFileObject. Mais son Ă©cosystĂšme, ses conventions, et ses frameworks restent centrĂ©s sur le Web.
Python a Ă©tĂ© pensĂ© pour l'automatisation universelle. Scripts systĂšme, data science, machine learning, scraping, orchestration d'infrastructure â des domaines oĂč les donnĂ©es sont massives et oĂč charger tout en mĂ©moire d'un coup n'est pas une option. Python est un langage de flux : il prĂ©fĂšre faire circuler la donnĂ©e plutĂŽt que la stocker. Ăa ne l'empĂȘche pas de faire du web (Flask, Django, FastAPI sont excellents) â mais lĂ encore, c'est une question d'ADN et d'Ă©cosystĂšme.
La diffĂ©rence n'est pas une limite technique â c'est une culture. Et cette culture explique pourquoi Python a naturalisĂ© les gĂ©nĂ©rateurs et les dĂ©corateurs au cĆur du langage, lĂ oĂč PHP les a ajoutĂ©s comme features optionnelles.
2. Le secret du yield : le marque-page
Pour comprendre ce que yield change vraiment, partons du problÚme. Sans générateur, la façon naïve de lire un fichier charge tout en mémoire d'un coup :
def lire_tout(fichier: str) -> list:
with open(fichier) as f:
return list(f) # list() force le chargement complet â tout en RAM immĂ©diatement
En Python moderne, for line in f: est dĂ©jĂ lazy par dĂ©faut â un fichier ouvert est un itĂ©rateur natif, il lit une ligne Ă la fois. list(f) est la façon explicite de forcer le chargement complet, ce qu'on fait ici uniquement pour montrer le contraste.
Avec yield, on exploite cette nature lazy et on la propage Ă la fonction entiĂšre :
def lire_flux(fichier: str):
with open(fichier) as f:
for ligne in f:
yield ligne.strip() # â Python s'arrĂȘte ici, rend la valeur, et attend
# au prochain next(), il reprend exactement ici
Ce "freeze" repose sur ce qu'on appelle un Stack Frame. Quand Python suspend une fonction générateur, il conserve en mémoire :
- La position exacte dans le code (Ă quelle ligne on s'est arrĂȘtĂ©)
- Les variables locales Ă cet instant (
ligne,f, etc.) - Le contexte d'exécution complet
C'est le marque-page : le livre reste ouvert Ă la bonne page. En pratique, pour un fichier de 10 millions de lignes :
## Approche liste â 10M de lignes chargĂ©es en RAM avant le premier print
lignes = lire_tout("access.log") # peut saturer la mémoire
for l in lignes: print(l)
## Approche gĂ©nĂ©rateur â 1 seule ligne en RAM Ă la fois
for l in lire_flux("access.log"): print(l) # mĂȘme rĂ©sultat, ~0 RAM supplĂ©mentaire
Approche liste â RAM : taille_fichier Ă taille_moyenne_ligne
Approche gĂ©nĂ©rateur â RAM : taille_d_une_ligne + overhead_stack_frame (â quelques Ko)
L'objet gĂ©nĂ©rateur lui-mĂȘme a un coĂ»t (sa stack frame suspendue). Pour un seul gĂ©nĂ©rateur c'est dĂ©risoire â mais crĂ©er des millions d'objets gĂ©nĂ©rateurs simultanĂ©ment a un petit coĂ»t non nul. ComparĂ© Ă une liste de 10M de lignes, c'est nĂ©gligeable.
Maintenant que le mĂ©canisme est clair : PHP a yield depuis PHP 5.5 et peut faire la mĂȘme chose. VoilĂ l'Ă©quivalent PHP :
// PHP â approche naturelle : tout en mĂ©moire
function lireFichier(string $chemin): array {
return file($chemin, FILE_IGNORE_NEW_LINES);
}
// PHP peut aussi faire du lazy avec SplFileObject â mais c'est moins idiomatique
function lireFlux(string $chemin): Generator {
$f = new SplFileObject($chemin);
while (!$f->eof()) {
yield $f->fgets();
}
}
La diffĂ©rence : en PHP, lireFlux est une curiositĂ© qu'on sort pour les gros fichiers. En Python, lire_flux est la façon normale de faire. Ce n'est pas une question de syntaxe â c'est une question de culture.
3. L'écosystÚme lazy : emballer et différer
Les dĂ©corateurs et les gĂ©nĂ©rateurs ne sont pas deux features indĂ©pendantes â ils incarnent la mĂȘme philosophie. Les deux rĂ©pondent Ă : comment Ă©viter de faire plus que nĂ©cessaire ?
Les décorateurs : emballer plutÎt que dupliquer
Sans dĂ©corateur, le mĂȘme code transversal se rĂ©pĂšte dans chaque fonction :
# Sans dĂ©corateur â rĂ©pĂ©tition, fragile, non-DRY
def parse_pip(raw: str):
debut = time.perf_counter()
# ... logique ...
print(f"parse_pip : {time.perf_counter() - debut:.4f}s")
def parse_gomod(raw: str):
debut = time.perf_counter()
# ... logique ...
print(f"parse_gomod : {time.perf_counter() - debut:.4f}s")
# Avec dĂ©corateur â emballĂ© une fois, appliquĂ© partout
@timer
def parse_pip(raw: str): ...
@timer
def parse_gomod(raw: str): ...
Le décorateur enveloppe un comportement transversal (logging, timing, retry, cache) sans polluer la logique métier. C'est l'approche Python du principe DRY.
Les générateurs : différer plutÎt que précalculer
# Eager â tout calculĂ© maintenant, mĂȘme si on n'utilise que le premier
carres = [x**2 for x in range(1_000_000)] # 1M éléments en RAM
premier = carres[0]
# Lazy â rien n'est calculĂ© tant qu'on n'y touche pas
carres = (x**2 for x in range(1_000_000)) # 0 calcul fait
premier = next(carres) # 1 seul calcul effectué
Les deux outils poussent dans la mĂȘme direction : ne faire que ce qui est nĂ©cessaire, au moment oĂč c'est nĂ©cessaire.
4. L'analogie du pipeline â dĂ©corateurs et gĂ©nĂ©rateurs ensemble
Imagine une chaĂźne de montage : chaque ouvrier reçoit une piĂšce, la transforme, la passe au suivant â sans attendre que toutes les piĂšces soient fabriquĂ©es. C'est ce que font des gĂ©nĂ©rateurs chaĂźnĂ©s. Et c'est lĂ qu'on voit les deux outils travailler ensemble.
import re
from utils.decorators import timer
def lire_lignes(fichier: str):
"""Ătape 1 â source : lit une ligne Ă la fois."""
with open(fichier) as f:
yield from f # délÚgue à l'itérateur natif du fichier
def filtrer_entetes(lignes):
"""Ătape 2 â filtre : ignore les lignes d'en-tĂȘte."""
for ligne in lignes:
if not ligne.startswith('#') and ligne.strip():
yield ligne
def extraire_composants(lignes):
"""Ătape 3 â transformation : parse chaque ligne."""
for ligne in lignes:
match = re.search(r"(\S+)\s+([\d\.]+)", ligne)
if match:
yield match.group(1), match.group(2)
# @timer + pipeline : le décorateur mesure le temps total car list()
# force l'exécution complÚte de tous les générateurs d'un coup
@timer
def traiter_fichier(fichier: str) -> list:
lignes = lire_lignes(fichier)
filtrees = filtrer_entetes(lignes)
composants = extraire_composants(filtrees)
return list(composants) # â c'est ici que les gĂ©nĂ©rateurs s'exĂ©cutent vraiment
composants = traiter_fichier("scanner.txt")
# [timer] traiter_fichier 0.0012s
for nom, version in composants:
print(f"{nom} â {version}")
Le @timer enveloppe la fonction qui consomme le pipeline â il mesure le temps total d'exĂ©cution parce que list() force les gĂ©nĂ©rateurs Ă s'exĂ©cuter jusqu'au bout. DĂ©corateurs et gĂ©nĂ©rateurs se complĂštent : les uns enveloppent le comportement, les autres diffĂšrent le calcul.
list(composants) :
list() demande une valeur Ă extraire_composants
â extraire_composants demande une ligne Ă filtrer_entetes
â filtrer_entetes demande une ligne Ă lire_lignes
â lire_lignes lit 1 ligne du fichier â remonte dans la chaĂźne
â filtrer_entetes filtre â remonte si ok
â extraire_composants parse â yield (nom, version)
â list() reçoit (nom, version) â recommence
with open dans un pipeline
Le with open dans lire_lignes reste ouvert tant que le gĂ©nĂ©rateur vit. Le fichier n'est fermĂ© que quand le pipeline est Ă©puisĂ© ou dĂ©truit. Si tu interromps prĂ©maturĂ©ment (break), Python garantit la fermeture via le garbage collector â mais le fichier reste ouvert pendant toute la durĂ©e de vie du pipeline.
Un pipeline lazy est Ă©lĂ©gant â mais plus difficile Ă dĂ©boguer. Si une erreur survient Ă l'Ă©tape 3, la stack trace remonte Ă travers tous les yield. Les donnĂ©es n'existent pas encore quand on construit le pipeline, donc on ne peut pas les inspecter avec un print() Ă mi-chemin.
Solution : convertir temporairement une Ă©tape en liste pour inspecter (list(filtrees)), puis re-basculer en gĂ©nĂ©rateur. Dans pyCycloneFlow, une erreur silencieuse dans un gĂ©nĂ©rateur peut produire un SBOM incomplet sans lever d'exception â d'oĂč l'importance des tests sur __wrapped__ vus au chapitre prĂ©cĂ©dent.
En Python, on ne cherche pas Ă stocker la donnĂ©e â on cherche Ă la faire circuler. Mais circuler avec rigueur.
Pour pyCycloneFlow, c'est exactement ce modĂšle : lire un fichier de scan, filtrer, parser, produire un SBOM â sans jamais charger l'ensemble en mĂ©moire. La donnĂ©e circule, se transforme, et sort en JSON conforme CycloneDX.