30  Les compréhensions

Les compréhensions sont probablement la construction la plus emblématique de Python. Elles permettent d’écrire en une ligne ce qui demanderait 4 ou 5 lignes avec une boucle classique. Maîtrisées au TOSA Avancé, elles font la différence entre un débutant et un Pythoniste.

30.1 Le principe

Vous avez déjà utilisé ce pattern dans la Partie 2, sans qu’on le nomme :

# Pattern classique : boucle + append
notes = [12, 15, 9, 17, 11]

carres = []
for n in notes:
    carres.append(n ** 2)

print(carres)
[144, 225, 81, 289, 121]

La compréhension de liste exprime la même chose en une ligne :

notes = [12, 15, 9, 17, 11]
carres = [n ** 2 for n in notes]
print(carres)
[144, 225, 81, 289, 121]

C’est plus court, plus lisible, et plus rapide (optimisations internes de Python).

Anatomie d’une compréhension

[ expression  for element in iterable  if condition ]
  └────┬────┘ └────────┬──────────┘  └─────┬──────┘
   transforme   parcourt chaque         filtre

Les trois parties :

  1. expression : ce qu’on produit pour chaque élément retenu.
  2. for element in iterable : d’où viennent les éléments.
  3. if condition (optionnelle) : garde seulement les éléments vérifiant la condition.

30.2 Compréhensions de liste

Forme basique : transformer

nombres = [1, 2, 3, 4, 5]

# Doubler
doubles = [n * 2 for n in nombres]
print(doubles)

# Carrés
carres = [n ** 2 for n in nombres]
print(carres)

# Conversion de types
chaines = [str(n) for n in nombres]
print(chaines)
[2, 4, 6, 8, 10]
[1, 4, 9, 16, 25]
['1', '2', '3', '4', '5']

Avec filtrage : if

nombres = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Seulement les pairs
pairs = [n for n in nombres if n % 2 == 0]
print(pairs)

# Pairs multipliés par 10
pairs_x10 = [n * 10 for n in nombres if n % 2 == 0]
print(pairs_x10)

# Chaînes non vides
mots = ["Alice", "", "Bob", "  ", "Charlie"]
non_vides = [m for m in mots if m.strip()]
print(non_vides)
[2, 4, 6, 8, 10]
[20, 40, 60, 80, 100]
['Alice', 'Bob', 'Charlie']

Avec expression conditionnelle (ternaire)

On peut placer une ternaire dans l’expression (attention, ce n’est plus un filtre !) :

notes = [12, 15, 9, 17, 7, 11]

# Étiqueter chaque note
mentions = ["admis" if n >= 10 else "recalé" for n in notes]
print(mentions)
['admis', 'admis', 'recalé', 'admis', 'recalé', 'admis']
⚠️ Piège classique : if avant ou après for ?

Il y a deux types de if dans une compréhension — attention à ne pas les confondre :

Position du if Rôle Syntaxe
Après for (à droite) Filtre [n for n in l if cond]
Avant for (ternaire dans l’expression) Choix de valeur [a if cond else b for n in l]
# Filtrer : ne garde que les pairs
print([n for n in range(6) if n % 2 == 0])

# Choisir : transformer selon condition
print(["pair" if n % 2 == 0 else "impair" for n in range(6)])

# Combiner les deux :
# filtrer (ne garder que > 5) ET transformer (* 10 ou * 100)
print([n * 10 if n > 7 else n * 100 for n in range(6, 12) if n != 10])
[0, 2, 4]
['pair', 'impair', 'pair', 'impair', 'pair', 'impair']
[600, 700, 80, 90, 110]

Compréhensions imbriquées (2 for)

On peut chaîner plusieurs for pour produire toutes les combinaisons :

# Produit cartésien
couleurs = ["rouge", "vert"]
tailles = ["S", "M", "L"]

combinaisons = [f"{c}-{t}" for c in couleurs for t in tailles]
print(combinaisons)
['rouge-S', 'rouge-M', 'rouge-L', 'vert-S', 'vert-M', 'vert-L']

Équivalent à deux boucles imbriquées :

combinaisons = []
for c in couleurs:
    for t in tailles:
        combinaisons.append(f"{c}-{t}")

Aplatir une liste de listes

Un idiome classique :

matrice = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

aplatie = [x for ligne in matrice for x in ligne]
print(aplatie)
[1, 2, 3, 4, 5, 6, 7, 8, 9]
Ordre des for : comme une lecture de gauche à droite

Dans [x for ligne in matrice for x in ligne], lisez de gauche à droite comme un code classique :

for ligne in matrice:
    for x in ligne:
        # ajouter x au résultat

C’est toujours le for le plus à gauche qui est le plus externe.

30.3 Compréhensions de dict

Même principe, mais on crée un dictionnaire avec {clé: valeur}.

noms = ["Alice", "Bob", "Charlie"]

# Créer un dict à partir d'une liste
longueurs = {nom: len(nom) for nom in noms}
print(longueurs)
{'Alice': 5, 'Bob': 3, 'Charlie': 7}

Transformer un dict

prix_ht = {"pain": 1.20, "lait": 0.95, "fromage": 4.50}

# Ajouter 20% de TVA
prix_ttc = {produit: prix * 1.20 for produit, prix in prix_ht.items()}
print(prix_ttc)

# Arrondir à 2 décimales
prix_ttc_arrondis = {p: round(pv, 2) for p, pv in prix_ttc.items()}
print(prix_ttc_arrondis)
{'pain': 1.44, 'lait': 1.14, 'fromage': 5.3999999999999995}
{'pain': 1.44, 'lait': 1.14, 'fromage': 5.4}

Inverser un dict (clé ↔︎ valeur)

d = {"a": 1, "b": 2, "c": 3}

inverse = {v: k for k, v in d.items()}
print(inverse)
{1: 'a', 2: 'b', 3: 'c'}

Avec filtre

notes = {"Alice": 14, "Bob": 8, "Charlie": 17, "Diana": 9, "Eve": 15}

# Ne garder que les admis
admis = {nom: n for nom, n in notes.items() if n >= 10}
print(admis)
{'Alice': 14, 'Charlie': 17, 'Eve': 15}

30.4 Compréhensions de set

Mêmes règles, avec {...} sans : :

mots = ["chat", "chien", "oiseau", "chat", "lapin", "chien"]

# Ensemble des mots uniques
uniques = {m for m in mots}
print(uniques)

# Longueurs uniques
longueurs = {len(m) for m in mots}
print(longueurs)

# Caractères uniques d'un texte
texte = "anticonstitutionnellement"
lettres = {c for c in texte}
print(lettres)
{'chien', 'chat', 'lapin', 'oiseau'}
{4, 5, 6}
{'t', 'e', 's', 'o', 'm', 'n', 'a', 'l', 'i', 'u', 'c'}
⚠️ Piège : {} vs compréhension
  • {1, 2, 3}set.
  • {"a": 1}dict.
  • {}dict vide (pas set vide !).
  • {n for n in ...}set (compréhension de set).
  • {n: n**2 for n in ...}dict (compréhension de dict, présence du :).

La présence du : dans l’expression distingue dict et set.

30.5 Expressions génératrices

Une expression génératrice s’écrit comme une compréhension de liste, mais avec des parenthèses (...) au lieu de crochets.

# Compréhension de liste : crée TOUTE la liste en mémoire
l = [n ** 2 for n in range(10)]
print(l)

# Expression génératrice : évalue À LA DEMANDE
g = (n ** 2 for n in range(10))
print(g)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
<generator object <genexpr> at 0x000001B414693510>

À quoi ça sert ?

Les générateurs ne stockent pas tous les éléments en mémoire — ils les produisent un à un pendant l’itération. Avantages :

  1. Économie mémoire massive sur de grands volumes.
  2. Lazy evaluation : on ne calcule que ce dont on a besoin.
# Comparaison mémoire
import sys

l = [n ** 2 for n in range(1_000_000)]
g = (n ** 2 for n in range(1_000_000))

print(f"Liste : {sys.getsizeof(l):,} octets")
print(f"Générateur : {sys.getsizeof(g):,} octets")
Liste : 8,448,728 octets
Générateur : 200 octets

Cas d’usage : passage direct à une fonction

Quand on passe une compréhension à une fonction comme sum, min, max, any, allles parenthèses de la fonction font office de parenthèses du générateur.

nombres = [1, 2, 3, 4, 5]

# Somme des carrés — sans liste intermédiaire
print(sum(n ** 2 for n in nombres))

# Tous les éléments positifs ?
print(all(n > 0 for n in nombres))

# Au moins un pair ?
print(any(n % 2 == 0 for n in nombres))
55
True
True

Pas besoin d’écrire sum([n ** 2 for n in nombres]) — c’est moins efficace (crée une liste temporaire inutile).

yield et les vrais générateurs (aperçu)

Les fonctions génératrices avec yield sont traitées au niveau Expert. Aperçu pour votre culture :

def carres_jusqu_a(n):
    """Renvoie les carrés de 0 à n-1."""
    for i in range(n):
        yield i ** 2       # yield au lieu de return

# Utilisation
g = carres_jusqu_a(5)
print(list(g))
[0, 1, 4, 9, 16]

On approfondira ce sujet en Partie 4.

30.6 Quand NE PAS utiliser une compréhension

Les compréhensions sont géniales, mais pas universelles. Évitez-les quand :

1. La logique est trop complexe

# ❌ Illisible — mieux vaut une boucle explicite
resultat = [traiter(x) if x > 0 else autre_traitement(x) if x == 0 else fallback(x)
            for x in donnees if pre_filtre(x)]

Règle du pouce : si votre compréhension fait plus d’une ligne, préférez une boucle classique.

2. Vous avez besoin de side effects

Les compréhensions sont faites pour construire une collection, pas pour des actions annexes (afficher, enregistrer…). Ne mettez pas print() dans une compréhension.

# ❌ Mauvais usage
[print(n) for n in [1, 2, 3]]   # crée une liste de None inutile

Préférez une boucle claire :

for n in [1, 2, 3]:
    print(n)
1
2
3

3. L’expression serait un effet de bord sur un dict/list externe

Si vous voulez modifier un dict existant, pas de compréhension.

30.7 Performance : compréhensions vs boucles

Les compréhensions sont typiquement 30 à 50 % plus rapides que les boucles équivalentes, car Python les optimise en interne.

import timeit

n = 10000

t_boucle = timeit.timeit('''
carres = []
for i in range(n):
    carres.append(i * i)
''', globals={"n": n}, number=1000)

t_compr = timeit.timeit('[i * i for i in range(n)]',
                        globals={"n": n}, number=1000)

print(f"Boucle       : {t_boucle*1000:.1f} ms")
print(f"Compréhension: {t_compr*1000:.1f} ms")
print(f"Gain : {(1 - t_compr/t_boucle)*100:.0f}% plus rapide")
Boucle       : 368.7 ms
Compréhension: 315.3 ms
Gain : 14% plus rapide

🧩 Quiz 1.1 — Les compréhensions

Question 1

Que renvoie [x * 2 for x in range(4)] ?

  1. [0, 2, 4, 6]
  2. [0, 1, 2, 3]
  3. [2, 4, 6, 8]
  4. [0, 2, 4, 6, 8]

a) [0, 2, 4, 6]range(4) donne 0, 1, 2, 3, chacun multiplié par 2.

print([x * 2 for x in range(4)])
[0, 2, 4, 6]

Question 2

Que renvoie [n for n in range(10) if n % 3 == 0] ?

  1. [0, 3, 6, 9]
  2. [3, 6, 9]
  3. [1, 2, 4, 5, 7, 8]
  4. [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

a) [0, 3, 6, 9] — on garde les multiples de 3 entre 0 et 9 (inclus). Attention : 0 est divisible par 3.

print([n for n in range(10) if n % 3 == 0])
[0, 3, 6, 9]

Question 3

Que produit {x: x**2 for x in range(1, 5)} ?

  1. {1: 1, 2: 4, 3: 9, 4: 16}
  2. {1, 4, 9, 16}
  3. [1, 4, 9, 16]
  4. Une erreur

a) {1: 1, 2: 4, 3: 9, 4: 16} — compréhension de dict : {clé: valeur for ...}. Le : dans l’expression indique qu’on crée un dict.

print({x: x**2 for x in range(1, 5)})
{1: 1, 2: 4, 3: 9, 4: 16}

Question 4

Quelle différence entre [n for n in range(3)] et (n for n in range(3)) ?

  1. Aucune, les deux renvoient une liste
  2. Le premier renvoie une liste, le second un tuple
  3. Le premier renvoie une liste, le second un générateur
  4. Le deuxième est une erreur de syntaxe

c) — crochets = liste (créée en mémoire), parenthèses = générateur (évaluation paresseuse, très économe en mémoire).

print(type([n for n in range(3)]))
print(type((n for n in range(3))))
<class 'list'>
<class 'generator'>

Question 5

Que produit [i + j for i in range(2) for j in range(3)] ?

  1. [0, 1, 2, 1, 2, 3]
  2. [0, 1, 2]
  3. [1, 2, 3]
  4. Une erreur

a) [0, 1, 2, 1, 2, 3] — pour i=0 : 0+0, 0+1, 0+2 = 0, 1, 2. Pour i=1 : 1+0, 1+1, 1+2 = 1, 2, 3.

print([i + j for i in range(2) for j in range(3)])
[0, 1, 2, 1, 2, 3]

Question 6

Quelle est la bonne syntaxe pour « n ou -n selon la parité » ?

  1. [n if n % 2 == 0 else -n for n in range(5)]
  2. [n for n in range(5) if n % 2 == 0 else -n]
  3. [n for n in range(5) if n % 2 == 0]
  4. [if n % 2 == 0 n else -n for n in range(5)]

a) — la ternaire A if cond else B vient avant le for. Piège classique : la forme if cond else dans la partie de gauche (transformation), la forme if cond à droite du for (filtre).

print([n if n % 2 == 0 else -n for n in range(5)])
[0, -1, 2, -3, 4]

Question 7

Quelle compréhension aplatit [[1, 2], [3, 4]] en [1, 2, 3, 4] ?

  1. [x for x in matrice]
  2. [x for ligne in matrice for x in ligne]
  3. [x for x in ligne for ligne in matrice]
  4. [flatten(matrice)]

b) — l’ordre des for compte : le plus externe (gauche) en premier. Lisez-la comme for ligne in matrice: for x in ligne: ajoute x.

matrice = [[1, 2], [3, 4]]
print([x for ligne in matrice for x in ligne])
[1, 2, 3, 4]

Question 8

Que renvoie sum(n for n in range(5)) ?

  1. Une erreur (pas de crochets)
  2. 10
  3. 15
  4. Un générateur

b) 10 — somme de 0+1+2+3+4. C’est une expression génératrice passée directement à sum ; les parenthèses de la fonction font office de parenthèses du générateur (pas besoin de les dédoubler).

print(sum(n for n in range(5)))
10

✏️ Exercice 1.1 — Conversions rapides

À partir de notes = [12, 15, 9, 17, 11, 14], produisez en une ligne pour chaque question :

  1. La liste des notes au format chaîne.
  2. La liste des notes en pourcentage (note × 5).
  3. La liste des notes uniques.
  4. Un dictionnaire {note: "admis"/"recalé"}.
notes = [12, 15, 9, 17, 11, 14]

# 1. Chaînes
...

# 2. Pourcentages
...

# 3. Notes uniques
...

# 4. Dict admis/recalé
...
notes = [12, 15, 9, 17, 11, 14, 12]    # avec un doublon pour Q3

# 1. Chaînes
chaines = [str(n) for n in notes]
print(chaines)

# 2. Pourcentages
pourcentages = [n * 5 for n in notes]
print(pourcentages)

# 3. Notes uniques
uniques = {n for n in notes}
print(uniques)

# 4. Dict admis/recalé
statuts = {n: ("admis" if n >= 10 else "recalé") for n in notes}
print(statuts)
['12', '15', '9', '17', '11', '14', '12']
[60, 75, 45, 85, 55, 70, 60]
{9, 11, 12, 14, 15, 17}
{12: 'admis', 15: 'admis', 9: 'recalé', 17: 'admis', 11: 'admis', 14: 'admis'}

✏️ Exercice 1.2 — Filtrer et transformer

Soit une liste de dictionnaires :

produits = [
    {"nom": "Pain",    "prix": 1.20, "bio": True},
    {"nom": "Lait",    "prix": 0.95, "bio": False},
    {"nom": "Fromage", "prix": 4.50, "bio": True},
    {"nom": "Vin",     "prix": 12.0, "bio": False},
    {"nom": "Miel",    "prix": 7.80, "bio": True},
]

En compréhensions exclusivement, produisez :

  1. La liste des noms de tous les produits.
  2. La liste des noms bio seulement.
  3. La liste des produits bio à moins de 10 €.
  4. Un dict {nom: prix_TTC} avec TVA à 5,5 % (bio) ou 20 % (non bio).
produits = [
    {"nom": "Pain",    "prix": 1.20, "bio": True},
    {"nom": "Lait",    "prix": 0.95, "bio": False},
    {"nom": "Fromage", "prix": 4.50, "bio": True},
    {"nom": "Vin",     "prix": 12.0, "bio": False},
    {"nom": "Miel",    "prix": 7.80, "bio": True},
]

# 1. Noms
...

# 2. Noms bio
...

# 3. Bio < 10€
...

# 4. Dict avec TVA variable
...
produits = [
    {"nom": "Pain",    "prix": 1.20, "bio": True},
    {"nom": "Lait",    "prix": 0.95, "bio": False},
    {"nom": "Fromage", "prix": 4.50, "bio": True},
    {"nom": "Vin",     "prix": 12.0, "bio": False},
    {"nom": "Miel",    "prix": 7.80, "bio": True},
]

# 1. Noms
noms = [p["nom"] for p in produits]
print("Tous :", noms)

# 2. Noms bio
noms_bio = [p["nom"] for p in produits if p["bio"]]
print("Bio :", noms_bio)

# 3. Bio < 10€
accessibles = [p for p in produits if p["bio"] and p["prix"] < 10]
print("Bio abordables :", [p["nom"] for p in accessibles])

# 4. Dict avec TVA variable
prix_ttc = {
    p["nom"]: round(p["prix"] * (1.055 if p["bio"] else 1.20), 2)
    for p in produits
}
print("Prix TTC :", prix_ttc)
Tous : ['Pain', 'Lait', 'Fromage', 'Vin', 'Miel']
Bio : ['Pain', 'Fromage', 'Miel']
Bio abordables : ['Pain', 'Fromage', 'Miel']
Prix TTC : {'Pain': 1.27, 'Lait': 1.14, 'Fromage': 4.75, 'Vin': 14.4, 'Miel': 8.23}

✏️ Exercice 1.3 — Matrice et opérations

Soit la matrice 3×3 :

matrice = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]

En compréhensions, produisez :

  1. La liste aplatie des 9 valeurs.
  2. La transposée (lignes ↔︎ colonnes).
  3. La liste des éléments pairs de la matrice.
  4. La somme de chaque ligne.
matrice = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]

# 1. Aplatir
...

# 2. Transposer
...

# 3. Éléments pairs
...

# 4. Somme de chaque ligne
...
matrice = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]

# 1. Aplatir
aplatie = [x for ligne in matrice for x in ligne]
print("Aplatie :", aplatie)

# 2. Transposer : lignes ↔ colonnes
# Pour chaque colonne j, construire une liste de matrice[i][j] pour chaque i
transposee = [[matrice[i][j] for i in range(3)] for j in range(3)]
print("Transposée :", transposee)

# 3. Pairs
pairs = [x for ligne in matrice for x in ligne if x % 2 == 0]
print("Pairs :", pairs)

# 4. Somme par ligne
sommes = [sum(ligne) for ligne in matrice]
print("Sommes :", sommes)
Aplatie : [1, 2, 3, 4, 5, 6, 7, 8, 9]
Transposée : [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
Pairs : [2, 4, 6, 8]
Sommes : [6, 15, 24]

Python offre une façon plus élégante de transposer avec zip :

matrice = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# * déballe la matrice en arguments séparés pour zip
transposee = [list(colonne) for colonne in zip(*matrice)]
print(transposee)
[[1, 4, 7], [2, 5, 8], [3, 6, 9]]

Le *matrice est un déballage (vu au chapitre 2). zip(*matrice) donne les colonnes.


✏️ Exercice 1.4 — Statistiques efficaces

À partir d’une liste de notes, sans boucle explicite, renvoyez un dict contenant nb_notes, moyenne, nb_admis, taux_reussite (en %).

def stats(notes):
    ...

print(stats([12, 15, 9, 17, 11, 14, 8]))
def stats(notes):
    if not notes:
        return {"nb_notes": 0, "moyenne": 0, "nb_admis": 0, "taux_reussite": 0}
    admis = sum(1 for n in notes if n >= 10)
    return {
        "nb_notes": len(notes),
        "moyenne": round(sum(notes) / len(notes), 2),
        "nb_admis": admis,
        "taux_reussite": round(admis / len(notes) * 100, 1),
    }

print(stats([12, 15, 9, 17, 11, 14, 8]))
print(stats([5, 4, 3]))
print(stats([]))
{'nb_notes': 7, 'moyenne': 12.29, 'nb_admis': 5, 'taux_reussite': 71.4}
{'nb_notes': 3, 'moyenne': 4.0, 'nb_admis': 0, 'taux_reussite': 0.0}
{'nb_notes': 0, 'moyenne': 0, 'nb_admis': 0, 'taux_reussite': 0}
  • sum(1 for n in notes if n >= 10) compte les notes vérifiant la condition, sans créer de liste intermédiaire.
  • Équivalent sum(n >= 10 for n in notes) — plus court, puisque True vaut 1.
  • Gestion du cas liste vide pour éviter la division par zéro.

À retenir

Points clés du chapitre
  1. Syntaxe : [expression for element in iterable if condition].
  2. Liste [], set {} (sans :), dict {:} (avec :), générateur ().
  3. Deux if : l’un à droite de for (filtre), l’autre dans l’expression (ternaire).
  4. Imbriquées : le for le plus à gauche est le plus externe.
  5. Générateurs : (...) — lazy, économes en mémoire. Idéal pour sum, any, all, max
  6. Évitez les compréhensions si la logique est complexe ou s’il y a des effets de bord.
  7. Les compréhensions sont 30-50 % plus rapides que les boucles équivalentes.

← Retour à la présentation du niveauChapitre suivant : *args et **kwargs →