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 :

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
La différence concrÚte :
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.

Ce qui se passe à chaque itération dans 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
Point subtil — 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.

Le revers de la médaille : le débogage

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.

Chapitre précédent Chapitre suivant : CLI argparse