# Une liste est un itérable
liste = [1, 2, 3]
# iter() obtient un itérateur depuis un itérable
it = iter(liste)
print(type(it))
# next() avance d'un élément
print(next(it))
print(next(it))
print(next(it))<class 'list_iterator'>
1
2
3
Vous utilisez des for depuis le début du cursus. Mais comment fonctionnent-ils en interne ? Comment Python sait-il parcourir une liste, un dict, un fichier ? La réponse : le protocole d’itération — un mécanisme qui permet d’écrire ses propres objets itérables, de produire des données à la demande, et de traiter des flux infinis. C’est la clé de la performance pour de gros volumes.
Deux notions distinctes à ne pas confondre.
Un itérable est un objet sur lequel on peut boucler avec for. Implémente __iter__() qui renvoie un itérateur.
Exemples d’itérables : list, tuple, str, dict, set, range, objets fichier…
Un itérateur est un objet qui produit les éléments un à un. Implémente __next__().
# Une liste est un itérable
liste = [1, 2, 3]
# iter() obtient un itérateur depuis un itérable
it = iter(liste)
print(type(it))
# next() avance d'un élément
print(next(it))
print(next(it))
print(next(it))<class 'list_iterator'>
1
2
3
# Quand l'itérateur est épuisé, next() lève StopIteration
it = iter([1, 2])
next(it); next(it)
next(it) # StopIteration--------------------------------------------------------------------------- StopIteration Traceback (most recent call last) Cell In[2], line 4 1 # Quand l'itérateur est épuisé, next() lève StopIteration 2 it = iter([1, 2]) 3 next(it); next(it) ----> 4 next(it) # StopIteration StopIteration:
for en coulissesfor x in liste:
print(x)…est équivalent à :
it = iter(liste)
while True:
try:
x = next(it)
except StopIteration:
break
print(x)Python cache cette complexité, mais c’est utile à connaître pour comprendre certains comportements.
On peut créer un objet itérable en implémentant __iter__ et __next__. Rarement nécessaire en pratique (les générateurs le font mieux), mais important à comprendre pour le TOSA.
class Compteur:
"""Itère de 1 à max (inclus)."""
def __init__(self, max):
self.max = max
self.courant = 0
def __iter__(self):
return self # l'objet est lui-même son itérateur
def __next__(self):
if self.courant >= self.max:
raise StopIteration
self.courant += 1
return self.courant
c = Compteur(5)
for x in c:
print(x, end=" ")1 2 3 4 5
Une fois épuisé, il reste épuisé. Pour re-parcourir, il faut recréer l’itérateur.
c = Compteur(3)
print(list(c)) # [1, 2, 3]
print(list(c)) # [] — épuisé ![1, 2, 3]
[]
Les listes (itérables) peuvent être re-parcourues parce que iter() crée un nouvel itérateur à chaque fois.
yieldLe yield transforme une fonction en générateur. Au lieu de calculer et retourner toute la liste, elle produit les éléments un par un, à la demande.
def compter_jusqu_a(n):
"""Générateur des entiers de 1 à n."""
i = 1
while i <= n:
yield i # produit i et SUSPEND la fonction
i += 1
# Usage comme n'importe quel itérable
for x in compter_jusqu_a(5):
print(x, end=" ")1 2 3 4 5
# On peut aussi le transformer en liste
print(list(compter_jusqu_a(5)))[1, 2, 3, 4, 5]
yieldyield n’est pas return. Quand Python rencontre yield :
next(), la fonction reprend exactement là où elle s’était arrêtée.def demo():
print(" [début]")
yield 1
print(" [après 1er yield]")
yield 2
print(" [après 2e yield]")
yield 3
print(" [fin]")
g = demo() # ne lance RIEN encore
print("→ appel de next :")
print(next(g)) # exécute jusqu'au 1er yield
print("→ appel de next :")
print(next(g))
print("→ appel de next :")
print(next(g))→ appel de next :
[début]
1
→ appel de next :
[après 1er yield]
2
→ appel de next :
[après 2e yield]
3
Voyez : la fonction reprend son exécution à chaque next, et les print intermédiaires s’exécutent au bon moment. C’est fondamentalement différent d’une fonction classique.
Une liste de 10 millions d’éléments occupe plusieurs centaines de Mo. Un générateur qui les produit à la demande : quelques centaines d’octets.
import sys
# Liste : tous les éléments en mémoire
liste_millions = [x ** 2 for x in range(10_000_000)]
print(f"Liste : {sys.getsizeof(liste_millions):,} octets")
# Générateur : quasi rien en mémoire
gen_millions = (x ** 2 for x in range(10_000_000))
print(f"Générateur : {sys.getsizeof(gen_millions):,} octets")Liste : 89,095,160 octets
Générateur : 200 octets
On a vu les expressions génératrices au chapitre 1 de la Partie 3 :
# Parenthèses = générateur, pas liste
carres = (x ** 2 for x in range(10))
print(carres)
print(list(carres))<generator object <genexpr> at 0x00000157462B4AD0>
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Elles sont équivalentes à une fonction générateur courte :
def carres_gen():
for x in range(10):
yield x ** 2def lire_lignes_non_vides(chemin):
"""Générateur : lit ligne par ligne, saute les vides."""
with open(chemin, "r", encoding="utf-8") as f:
for ligne in f:
ligne = ligne.strip()
if ligne:
yield ligne
# Usage : mémoire constante, même pour un fichier de 10 Go
for ligne in lire_lignes_non_vides("gros_fichier.txt"):
traiter(ligne)Un générateur peut ne jamais s’arrêter. Pas de problème tant qu’on ne le transforme pas en liste.
def entiers():
"""Suite infinie 1, 2, 3, 4..."""
n = 1
while True:
yield n
n += 1
# Prendre les 10 premiers
from itertools import islice
print(list(islice(entiers(), 10)))[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Exemple classique : générer les nombres de Fibonacci à la demande.
def fibonacci():
"""Générateur infini de Fibonacci."""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# 20 premiers
from itertools import islice
print(list(islice(fibonacci(), 20)))[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181]
On peut chaîner des générateurs pour traiter un flux par étapes, sans stocker les intermédiaires.
def nombres():
for i in range(1, 11):
yield i
def carres(source):
for n in source:
yield n ** 2
def pairs(source):
for n in source:
if n % 2 == 0:
yield n
# Chaînage : nombres → carrés → pairs
resultat = pairs(carres(nombres()))
print(list(resultat))[4, 16, 36, 64, 100]
Chaque étape produit un élément à la fois. Aucun stockage intermédiaire — idéal pour des flux énormes.
yield from — déléguer à un autre générateuryield from (Python 3.3+) permet de déléguer l’itération à un autre itérable.
def compter(debut, fin):
yield from range(debut, fin)
print(list(compter(1, 5)))[1, 2, 3, 4]
Équivalent à :
def compter(debut, fin):
for x in range(debut, fin):
yield xUtile pour composer des générateurs :
def chaine(*iterables):
"""Comme itertools.chain : enchaîne plusieurs itérables."""
for it in iterables:
yield from it
print(list(chaine([1, 2, 3], (4, 5), "AB")))[1, 2, 3, 4, 5, 'A', 'B']
itertoolsitertools est une bibliothèque de briques réutilisables pour composer des itérateurs. Extrêmement testé au TOSA Expert.
count, cycle, repeat — infinisfrom itertools import count, cycle, repeat, islice
# count : suite arithmétique infinie
print(list(islice(count(10, 2), 5))) # 10, 12, 14, 16, 18
# cycle : répète un itérable indéfiniment
print(list(islice(cycle([1, 2, 3]), 8))) # 1 2 3 1 2 3 1 2
# repeat : répète une valeur, optionnellement n fois
print(list(repeat("Hello", 3)))[10, 12, 14, 16, 18]
[1, 2, 3, 1, 2, 3, 1, 2]
['Hello', 'Hello', 'Hello']
chain — enchaîner des itérablesfrom itertools import chain
print(list(chain([1, 2], (3, 4), "AB")))[1, 2, 3, 4, 'A', 'B']
Équivalent à notre yield from mais builtin — et plus optimisé.
islice — slicer un itérableComme liste[start:stop:step] mais pour n’importe quel itérable (utile pour les infinis !).
from itertools import islice, count
# Éléments 5 à 10 d'un compteur infini
print(list(islice(count(), 5, 10)))
# Les 3 premiers d'un itérable
from itertools import cycle
print(list(islice(cycle("abc"), 7)))[5, 6, 7, 8, 9]
['a', 'b', 'c', 'a', 'b', 'c', 'a']
takewhile, dropwhiletakewhile(pred, it) : prend les éléments tant que pred(x) est vrai, puis s’arrête.dropwhile(pred, it) : ignore les éléments tant que vrai, puis garde tout le reste.from itertools import takewhile, dropwhile
nombres = [1, 3, 5, 2, 8, 9, 1]
# Prendre tant que < 5
print(list(takewhile(lambda x: x < 5, nombres)))
# Ignorer tant que < 5
print(list(dropwhile(lambda x: x < 5, nombres)))[1, 3]
[5, 2, 8, 9, 1]
Attention : ce n’est pas un filtre. Dès que la condition devient fausse (pour takewhile) ou vraie (pour dropwhile), le comportement bascule et ne revient pas.
zip_longest — zip qui ne s’arrête pas au plus courtRappel : zip s’arrête dès qu’un itérable est épuisé. zip_longest continue en utilisant une valeur de remplissage.
from itertools import zip_longest
noms = ["Alice", "Bob", "Charlie"]
ages = [30, 25]
print(list(zip(noms, ages))) # s'arrête à 2
print(list(zip_longest(noms, ages, fillvalue=0))) # complète à 0[('Alice', 30), ('Bob', 25)]
[('Alice', 30), ('Bob', 25), ('Charlie', 0)]
product — produit cartésienToutes les combinaisons possibles — équivalent à plusieurs for imbriqués.
from itertools import product
couleurs = ["rouge", "vert"]
tailles = ["S", "M", "L"]
for c, t in product(couleurs, tailles):
print(f"{c}-{t}", end=" ")
print()
# product(it, repeat=n) : n fois le même itérable
print(list(product("AB", repeat=2)))rouge-S rouge-M rouge-L vert-S vert-M vert-L
[('A', 'A'), ('A', 'B'), ('B', 'A'), ('B', 'B')]
permutations, combinationsfrom itertools import permutations, combinations
# Permutations : l'ordre compte
print(list(permutations("ABC", 2)))
# Combinations : l'ordre ne compte PAS
print(list(combinations("ABC", 2)))[('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
[('A', 'B'), ('A', 'C'), ('B', 'C')]
groupby — regrouper les éléments consécutifsRegroupe les éléments consécutifs selon une clé.
from itertools import groupby
donnees = "aaabbbccddddeea"
for cle, groupe in groupby(donnees):
print(f"{cle} x{len(list(groupe))}", end=" ")a x3 b x3 c x2 d x4 e x2 a x1
groupby ne trie pas
groupby regroupe seulement les éléments adjacents. Si vous voulez grouper tout ce qui est identique, triez d’abord.
from itertools import groupby
personnes = [
{"nom": "Alice", "ville": "Paris"},
{"nom": "Bob", "ville": "Lyon"},
{"nom": "Eve", "ville": "Paris"},
]
# ❌ Sans tri : Alice et Eve ne se regrouperont pas
for ville, groupe in groupby(personnes, key=lambda p: p["ville"]):
print(f"{ville} : {[p['nom'] for p in groupe]}")
# ✅ Avec tri
print("---")
personnes_triees = sorted(personnes, key=lambda p: p["ville"])
for ville, groupe in groupby(personnes_triees, key=lambda p: p["ville"]):
print(f"{ville} : {[p['nom'] for p in groupe]}")Paris : ['Alice']
Lyon : ['Bob']
Paris : ['Eve']
---
Lyon : ['Bob']
Paris : ['Alice', 'Eve']
accumulate — sommes cumulées (ou autre)from itertools import accumulate
import operator
# Somme cumulée
print(list(accumulate([1, 2, 3, 4])))
# Maximum cumulé
print(list(accumulate([3, 1, 4, 1, 5, 9, 2], max)))
# Produit cumulé
print(list(accumulate([1, 2, 3, 4], operator.mul)))[1, 3, 6, 10]
[3, 3, 4, 4, 5, 9, 9]
[1, 2, 6, 24]
| Fonction | Utilité |
|---|---|
count(start, step) |
Compteur infini |
cycle(iterable) |
Répétition infinie |
repeat(valeur, n) |
Répète une valeur |
chain(*iterables) |
Enchaîne plusieurs itérables |
islice(it, stop) |
Slice d’itérable |
takewhile(pred, it) |
Prend tant que vrai |
dropwhile(pred, it) |
Ignore tant que vrai |
zip_longest(*its) |
zip sans s’arrêter au court |
product(*its) |
Produit cartésien |
permutations(it, r) |
Permutations |
combinations(it, r) |
Combinaisons |
groupby(it, key) |
Regroupement consécutif |
accumulate(it, func) |
Accumulation (somme, max…) |
Quelle différence entre un itérable et un itérateur ?
b) — l’itérable (list, dict, str…) répond à iter() et peut être parcouru plusieurs fois. L’itérateur répond à next() et ne peut être parcouru qu’une fois.
Que renvoie cette fonction ?
def f():
yield 1
yield 2
yield 3
x = f()
type(x)<class 'list'><class 'generator'><class 'function'><class 'tuple'>b) <class 'generator'> — la présence de yield transforme la fonction en fabrique de générateurs. L’appel renvoie un objet générateur.
def f():
yield 1
print(type(f()))<class 'generator'>
Que s’affiche-t-il ?
g = (x * 2 for x in range(3))
print(list(g))
print(list(g))[0, 2, 4] puis [0, 2, 4][0, 2, 4] puis [][0, 2, 4] puis une erreur[] puis [0, 2, 4]b) — un générateur est à usage unique. Le premier list(g) le consomme ; le second le trouve vide.
g = (x * 2 for x in range(3))
print(list(g))
print(list(g))[0, 2, 4]
[]
Que renvoie list(itertools.chain([1, 2], [3, 4])) ?
[[1, 2], [3, 4]][1, 2, 3, 4][(1, 3), (2, 4)]b) [1, 2, 3, 4] — chain enchaîne plusieurs itérables comme s’ils étaient un seul.
from itertools import chain
print(list(chain([1, 2], [3, 4])))[1, 2, 3, 4]
Quelle fonction prend les n premiers éléments d’un itérable infini ?
list(it)[:n]it[:n]itertools.islice(it, n)itertools.takewhile(it, n)c) islice(it, n) — list(it)[:n] plante sur un itérable infini (on essaie de tout transformer en liste). it[:n] ne fonctionne que sur des séquences (pas les itérateurs). islice est la bonne réponse.
from itertools import count, islice
print(list(islice(count(1), 5)))[1, 2, 3, 4, 5]
Que renvoie list(itertools.combinations("ABC", 2)) ?
[("A", "B"), ("A", "C"), ("B", "C")][("A", "B"), ("A", "C"), ("B", "C"), ("B", "A"), ("C", "A"), ("C", "B")][("A", "A"), ("B", "B"), ("C", "C")]["AB", "AC", "BC"]a) — combinations ne garde que les combinaisons sans ordre (pas de doublons avec ordre inversé). permutations donnerait la réponse b.
from itertools import combinations, permutations
print(list(combinations("ABC", 2)))
print(list(permutations("ABC", 2)))[('A', 'B'), ('A', 'C'), ('B', 'C')]
[('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
Quel est le principal avantage des générateurs sur les listes ?
b) — un générateur produit un élément à la fois, sans jamais stocker la séquence complète en mémoire. Indispensable pour des flux massifs ou infinis.
Que fait yield from [1, 2, 3] ?
yield successifs : yield 1; yield 2; yield 3return [1, 2, 3]b) — yield from iter équivaut à for x in iter: yield x. Permet de déléguer à un sous-itérable.
def f():
yield from [1, 2, 3]
yield from "AB"
print(list(f()))[1, 2, 3, 'A', 'B']
Écrivez un générateur pairs_jusqu_a(n) qui renvoie les entiers pairs de 2 à n inclus.
def pairs_jusqu_a(n):
...
print(list(pairs_jusqu_a(10))) # [2, 4, 6, 8, 10]
print(list(pairs_jusqu_a(0))) # []def pairs_jusqu_a(n):
i = 2
while i <= n:
yield i
i += 2
print(list(pairs_jusqu_a(10)))
print(list(pairs_jusqu_a(0)))
print(list(pairs_jusqu_a(1)))[2, 4, 6, 8, 10]
[]
[]
def pairs_jusqu_a(n):
yield from range(2, n + 1, 2)
print(list(pairs_jusqu_a(10)))[2, 4, 6, 8, 10]
Écrivez un générateur premiers() infini qui produit les nombres premiers. Utilisez islice pour obtenir les 20 premiers.
from itertools import islice
def est_premier(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
def premiers():
...
print(list(islice(premiers(), 20)))from itertools import islice
def est_premier(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
def premiers():
"""Générateur infini des nombres premiers."""
n = 2
while True:
if est_premier(n):
yield n
n += 1
# 20 premiers nombres premiers
print(list(islice(premiers(), 20)))
# Ou : les premiers < 100
from itertools import takewhile
print(list(takewhile(lambda p: p < 100, premiers())))[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71]
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
premiers() est infini — il ne s’arrêtera jamais tout seul.islice pour borner à n valeurs, takewhile pour borner par une condition.Créez un pipeline de générateurs qui :
.strip()).Chaque étape doit être un générateur séparé.
lignes_source = [
" alice ",
"",
"bob",
" charlotte",
" ",
"diana",
"eve",
]
def source(lignes): ...
def nettoyer(it): ...
def majuscules(it): ...
def filtrer_longues(it, mini=5): ...
# Chaînage
pipeline = filtrer_longues(majuscules(nettoyer(source(lignes_source))))
print(list(pipeline))def source(lignes):
for ligne in lignes:
yield ligne
def nettoyer(it):
for ligne in it:
strippee = ligne.strip()
if strippee:
yield strippee
def majuscules(it):
for ligne in it:
yield ligne.upper()
def filtrer_longues(it, mini=5):
for ligne in it:
if len(ligne) > mini:
yield ligne
lignes_source = [
" alice ",
"",
"bob",
" charlotte",
" ",
"diana",
"eve",
]
pipeline = filtrer_longues(majuscules(nettoyer(source(lignes_source))))
print(list(pipeline))['CHARLOTTE']
cmd1 | cmd2 | cmd3).groupbyUtilisez itertools.groupby pour compter les occurrences consécutives dans une chaîne (RLE — Run-Length Encoding).
Exemple : "aaabbc" → [('a', 3), ('b', 2), ('c', 1)].
from itertools import groupby
def compresser(chaine):
...
print(compresser("aaabbc"))
print(compresser("aabbbccccd"))
print(compresser(""))from itertools import groupby
def compresser(chaine):
return [(c, len(list(groupe))) for c, groupe in groupby(chaine)]
print(compresser("aaabbc"))
print(compresser("aabbbccccd"))
print(compresser(""))
print(compresser("abba")) # attention : 2 groupes pour 'a'[('a', 3), ('b', 2), ('c', 1)]
[('a', 2), ('b', 3), ('c', 4), ('d', 1)]
[]
[('a', 1), ('b', 2), ('a', 1)]
def decompresser(paires):
return "".join(c * n for c, n in paires)
compresse = compresser("aaabbc")
print(compresse)
print(decompresser(compresse))[('a', 3), ('b', 2), ('c', 1)]
aaabbc
groupby ne trie pas. Il regroupe les consécutifs. D’où "abba" donne 3 groupes.groupby est un itérateur lui aussi. On doit le consommer (avec list ou len(list(...))) dans la même itération.iter() (liste, tuple, str, dict…). Itérateur : répond à next().for x in it: est équivalent à while True: try: x = next(it); except StopIteration: break.yield. Produit à la demande, économie mémoire massive.yield suspend la fonction ; next() reprend à l’endroit exact.(x for x in ...) : parenthèses au lieu de crochets.yield from délègue à un sous-itérable.itertools : count, cycle, chain, islice, takewhile, dropwhile, zip_longest, product, permutations, combinations, groupby, accumulate.groupby ne trie pas — triez avant si besoin.← Chapitre précédent : Regex • Chapitre suivant : Les décorateurs →