Séance 17 · Partie C · Python avancé
Itérateurs, générateurs,
compréhensions.
Un capteur produit des mesures sans fin. Un fichier de 10 Go ne tient pas en mémoire. Python a une réponse élégante : produire les valeurs une à la fois, à la demande.
Durée : 90 min · Objectifs : compréhensions de liste/dict/ensemble, le protocole d’itération, yield et les générateurs, expressions génératrices, itertools, fonctions d’ordre supérieur et décorateurs.
Ce que vous saurez faire à la fin
- Écrire du Python compact et lisible avec les compréhensions.
- Traiter un flux infini ou un fichier énorme sans le charger en mémoire.
- Comprendre ce que fait vraiment
for. - Écrire un décorateur simple (chronométrage, cache).
01 / Compréhensions
Liste, dictionnaire, ensemble : la même syntaxe
Lisibilité avant tout
Une compréhension se lit « la liste des f(x) pour x dans … si … ». Elle remplace 4 lignes de boucle + append. Au-delà de deux for ou d’une condition compliquée, revenez à une boucle : la compréhension n’est pas plus rapide au point de justifier l’illisibilité. Un ensemble (set) teste l’appartenance en O(1), comme un dictionnaire sans valeurs : x in ensemble est instantané, x in liste est O(n).
02 / Itération
Ce que fait vraiment un for
Le protocole
for x in obj fait : it = iter(obj) puis répète x = next(it) jusqu’à StopIteration. Un objet itérable a __iter__ ; un itérateur a en plus __next__. Une liste crée un nouvel itérateur à chaque for (on peut la parcourir deux fois) ; un itérateur, lui, s’épuise. Ce protocole unique explique pourquoi sum, max, list, ", ".join acceptent tous n’importe quelle source.
02 / Itération
yield : écrire un itérateur en trois lignes
Un générateur, c’est une fonction qui se souvient
Une fonction ordinaire s’exécute d’un bloc et oublie tout. Une fonction contenant yield renvoie un générateur : chaque next() exécute le code jusqu’au prochain yield, renvoie la valeur, et gèle l’état (variables locales, position). Rien n’est calculé avant qu’on le demande : c’est l’évaluation paresseuse. On peut donc décrire une suite infinie et n’en consommer que ce dont on a besoin. C’est ce que fait range.
02 / Itération
Traiter un flux : la chaîne de générateurs
Pourquoi c’est puissant
Quatre étapes indépendantes, chacune testable seule, assemblées comme des tuyaux. Aucune liste intermédiaire : la mémoire est constante, quelle que soit la durée du flux. Sur un robot, chaque étage devient un filtre temps réel. C’est aussi l’architecture des outils Unix (cat | grep | sort) et des frameworks de traitement de données. Le vocabulaire : pipeline, stream processing.
02 / Itération
Expressions génératrices et itertools
pairwise et accumulate en robotique
pairwise donne les couples consécutifs : parfait pour calculer des vitesses à partir de positions ((b − a) / dt), ou des distances entre points d’une trajectoire. accumulate intègre : des vitesses vers des positions. groupby détecte les plages consécutives d’un même état (« le robot a été bloqué pendant 12 cycles »).
03 / Fonctions
Les fonctions sont des valeurs
Fermetures
f se souvient de k même après que multiplicateur a terminé : on dit qu’elle capture son environnement. C’est le mécanisme derrière les contrôleurs de la séance 15 (proportionnel(kp) renvoyait une lambda capturant kp). Une fermeture est une alternative légère à un objet quand on n’a qu’une méthode. map/filter sont souvent remplacés par des compréhensions, plus lisibles.
03 / Fonctions
Décorateurs : modifier une fonction sans la toucher
*args, **kwargs
*args capture tous les arguments positionnels dans un tuple, **kwargs les arguments nommés dans un dictionnaire : l’enveloppe accepte donc n’importe quelle signature et la transmet. Un décorateur est une fonction qui prend une fonction et en renvoie une autre ; le @ n’est que du sucre. Usages courants : chronométrer, mettre en cache, vérifier des droits, réessayer en cas d’erreur, enregistrer les appels. @dataclass et @property (séance 16) sont des décorateurs.
04 / Défis
Défi ★ — Réécrire avec des compréhensions
Consigne
Réécrire chaque bloc en une seule expression (compréhension ou générateur) : 1. les longueurs des mots d’une phrase ; 2. le dictionnaire mot → longueur, seulement pour les mots de plus de 3 lettres ; 3. la somme des carrés des impairs de 1 à 99 ; 4. la matrice identité 4×4 ; 5. la transposée d’une matrice (liste de listes).
Correction
longueurs = [len(mot) for mot in phrase.split()]
d = {mot: len(mot) for mot in phrase.split() if len(mot) > 3}
s = sum(n * n for n in range(1, 100, 2))
identite = [[1 if i == j else 0 for j in range(4)] for i in range(4)]
transposee = [[ligne[j] for ligne in M] for j in range(len(M[0]))]
# ou : transposee = [list(col) for col in zip(*M)]zip(*M) : l’étoile « déballe » les lignes comme arguments séparés, et zip les regroupe colonne par colonne. Idiome à connaître.
04 / Défis
Défi ★★ — Générateurs pour un fichier énorme
Consigne
Le code crée un fichier de 200 000 lignes « temps,distance ». Écrire, avec des générateurs et sans jamais charger tout le fichier en mémoire : lire_mesures(chemin) qui produit des tuples (t, d) ; detecter_chutes(flux, seuil) qui produit les instants où la distance chute de plus de seuil entre deux mesures consécutives ; puis compter ces chutes et afficher les 5 premières.
Correction
def lire_mesures(chemin):
with open(chemin) as f:
for ligne in f:
t, d = ligne.split(",")
yield int(t), float(d)
def detecter_chutes(flux, seuil=20):
precedent = None
for t, d in flux:
if precedent is not None and precedent - d > seuil:
yield t, precedent - d
precedent = d
chutes = list(detecter_chutes(lire_mesures("gros.csv")))
print(len(chutes), "chutes ; premières :", chutes[:5])
# Variante avec itertools.pairwise :
# chutes = [(t2, d1 - d2) for (t1, d1), (t2, d2) in itertools.pairwise(lire_mesures("gros.csv")) if d1 - d2 > 20]Le with à l’intérieur du générateur : le fichier reste ouvert tant que le générateur est consommé et se ferme quand il est épuisé. Environ 200 chutes détectées. Ce programme traiterait un fichier de 100 Go avec la même mémoire.
04 / Défis
Défi ★★★ — Esprit prépa : le crible paresseux et le décorateur de réessai
Consigne
1. Écrire premiers(), un générateur infini de nombres premiers, en gardant la liste des premiers déjà trouvés et en ne testant que la divisibilité par ceux ≤ √n. Afficher le 1000ᵉ premier. Comparer le temps avec le crible de la séance 06 pour la même cible.
2. Écrire un décorateur reessayer(tentatives=3, exceptions=(Exception,)) qui relance la fonction si elle lève une de ces exceptions, jusqu’à tentatives fois, puis propage l’erreur. Le tester avec une fonction qui échoue aléatoirement 60 % du temps. (C’est exactement ce qu’on fait pour lire un capteur capricieux.)
Correction et ouverture
def premiers():
trouves = []
n = 2
while True:
if all(n % p for p in itertools.takewhile(lambda p: p * p <= n, trouves)):
trouves.append(n)
yield n
n += 1
import itertools
print(next(itertools.islice(premiers(), 999, None))) # 7919
def reessayer(tentatives=3, exceptions=(Exception,)):
def decorateur(f):
@functools.wraps(f)
def enveloppe(*a, **k):
for i in range(1, tentatives + 1):
try:
return f(*a, **k)
except exceptions as e:
print(f" tentative {i} échouée : {e}")
if i == tentatives:
raise
return enveloppe
return decorateur
@reessayer(tentatives=5, exceptions=(IOError,))
def lire_capteur():
if random.random() < 0.6:
raise IOError("pas de réponse")
return 42
print(lire_capteur())Un décorateur avec paramètres est une fonction qui renvoie un décorateur : trois niveaux de def. takewhile s’arrête au premier premier > √n : on ne parcourt pas toute la liste. Le générateur de premiers garde une mémoire croissante mais ne fixe aucune limite à l’avance, contrairement au crible ; il est plus lent (quelques dizaines de ms pour le 1000ᵉ) mais infini. Ouverture : cherchez « crible d’Ératosthène incrémental » (D. O’Neill, 2009), une version paresseuse du crible avec un dictionnaire de multiples, plus rapide que celle-ci.
05 / Vérification
Quelle différence entre [x*x for x in range(10**7)] et (x*x for x in range(10**7)) ?
Deux questions supplémentaires
1. Peut-on parcourir deux fois un générateur ? Non : une fois épuisé, il ne produit plus rien. Il faut le recréer.
2. Que fait @f au-dessus de def g ? g = f(g) : remplace g par ce que f renvoie.
Référence
Les mots à retenir
| Mot | Définition |
|---|---|
| Compréhension | [f(x) for x in it if c], aussi pour dict et set. |
| Itérable / itérateur | A __iter__ / a aussi __next__. |
| Générateur | Fonction avec yield ; produit à la demande. |
| Évaluation paresseuse | Calculer seulement quand la valeur est demandée. |
| Fermeture | Fonction qui capture des variables de son contexte. |
| Décorateur | Fonction qui transforme une fonction ; syntaxe @. |
| Pipeline | Chaîne de générateurs, chacun transformant le flux. |
Pour continuer
Votre Python est devenu élégant
Séance suivante : les structures de données qui font réfléchir un robot — piles, files, graphes — et le plus court chemin.
À faire chez soi
- Réécrire les filtres de la séance 09 (médiane glissante) sous forme de générateur.
- Écrire
@memoriser(votre proprelru_cache) avec un dictionnaire. - Écrire un générateur
lignes_non_vides(chemin)et l’utiliser pour compter les lignes de code d’un fichier Python (sans commentaires ni lignes vides).