backend / python
Compréhensions : list, dict, set, generator
Explication
Ce que vous allez apprendre
- Transformer une boucle
for+.append()en une compréhension de liste lisible - Écrire des compréhensions de dict et de set pour construire ces structures en une ligne
- Distinguer
[...](liste immédiate) de(...)(générateur paresseux) et savoir quand utiliser lequel - Reconnaître le seuil de complexité au-delà duquel une compréhension nuit à la lisibilité
- Utiliser le walrus operator (
:=) pour éviter de recalculer une valeur dans un filtre
Dans quel contexte ?
Un développeur doit extraire, depuis une liste de dictionnaires commandes, uniquement les identifiants des commandes dont le statut vaut "livree". Écrire une boucle for avec une liste vide puis .append() fonctionne, mais [c["id"] for c in commandes if c["statut"] == "livree"] exprime la même idée en une ligne, plus proche de la façon dont on formulerait la demande à voix haute.
Construire une collection en une phrase
Une compréhension, c'est la traduction directe en une seule ligne d'une idée du type je veux, pour chaque élément d'une collection qui vérifie telle condition, telle transformation. Plutôt que d'écrire une boucle for qui crée une liste vide puis l'alimente avec .append(), on exprime directement le résultat voulu. Ce n'est pas qu'une question d'élégance : le code Python interne d'une compréhension est optimisé et généralement plus rapide qu'une boucle explicite équivalente.
Le piège de la lisibilité
Une compréhension reste géniale tant qu'elle reste lisible d'un coup d'œil. Dès qu'elle empile plusieurs boucles imbriquées et plusieurs conditions, elle devient plus difficile à comprendre qu'une boucle classique bien nommée. La règle de bon sens : au-delà de deux niveaux d'imbrication, mieux vaut revenir à un for traditionnel, quitte à perdre en concision ce qu'on gagne en clarté.
Piège fréquent
[y for x in matrice for y in x if y > 0 if y % 2 == 0] fonctionne, mais personne ne la relit sans effort en revue de code. Si une compréhension dépasse une ligne d'écran ou empile plus de deux clauses, mieux vaut la réécrire en boucle for classique avec des noms de variables explicites.
Générateur contre liste : une distinction cruciale
Les crochets [...] créent immédiatement toute la collection en mémoire. Les parenthèses (...) créent un générateur, qui ne produit ses valeurs qu'à la demande, une par une, sans jamais stocker l'ensemble en mémoire. Pour sommer un million de carrés, la différence de consommation mémoire entre les deux approches est spectaculaire : quelques dizaines d'octets contre plusieurs dizaines de mégaoctets.
| Syntaxe | Type produit | Mémoire utilisée |
|---|---|---|
[x**2 for x in range(n)] | list | Proportionnelle à n |
(x**2 for x in range(n)) | générateur | Quasi constante |
{x**2 for x in range(n)} | set | Proportionnelle à n (dédupliquée) |
{x: x**2 for x in range(n)} | dict | Proportionnelle à n |
Le lien avec les leçons suivantes
Cette leçon annonce directement le sujet des itérateurs et générateurs (leçon 14), où l'on comprendra en profondeur ce mécanisme de paresse qui permet de traiter des volumes de données bien plus grands que la mémoire disponible.
Commandes & code
Compréhensions
Construire des collections de facon concise et idiomatique.
# List comprehension : forme de base
carres = [x**2 for x in range(10)]
# Avec condition (filtre)
pairs = [x for x in range(20) if x % 2 == 0]
# Avec condition ternaire (transformation conditionnelle)
etiquettes = ["pair" if x % 2 == 0 else "impair" for x in range(6)]
# Compréhension imbriquée : aplatir une matrice
matrice = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
aplatie = [valeur for ligne in matrice for valeur in ligne]
# Double boucle avec filtre : produit cartesien filtre
paires_valides = [(x, y) for x in range(4) for y in range(4) if x != y]
# Dict comprehension
carres_dict = {x: x**2 for x in range(6)}
inverse = {v: k for k, v in carres_dict.items()} # inverser cles/valeurs
# Filtrer un dict existant
utilisateurs = {"alice": 30, "bob": 17, "cy": 45}
majeurs = {nom: age for nom, age in utilisateurs.items() if age >= 18}
# Set comprehension
longueurs_uniques = {len(mot) for mot in ["chat", "chien", "abcd", "os"]}
# Generator expression : paresseux, ne stocke rien en memoire (parentheses)
somme_carres = sum(x**2 for x in range(1_000_000)) # pas de liste intermediaire creee
plus_grand = max((len(mot) for mot in ["a", "bb", "ccc"]), default=0)
# Difference cruciale : liste vs generateur
liste_en_memoire = [x**2 for x in range(10_000_000)] # ~ plusieurs dizaines de Mo
generateur_paresseux = (x**2 for x in range(10_000_000)) # quelques dizaines d'octets
# Compréhensions imbriquées avancées : matrice transposee
matrice = [[1, 2, 3], [4, 5, 6]]
transposee = [[ligne[i] for ligne in matrice] for i in range(len(matrice[0]))]
print(transposee) # [[1, 4], [2, 5], [3, 6]]
# walrus operator (:=) dans une comprehension : evite un appel double
donnees = [1, -2, 3, -4, 5, -6]
resultats = [carre for x in donnees if (carre := x ** 2) > 5]
# Attention a la lisibilite : au-dela de 2 niveaux, preferer une boucle classique
def traiter_donnees_complexe(items):
resultat = []
for item in items:
if not item.get("actif"):
continue
for tag in item.get("tags", []):
if tag.startswith("prio"):
resultat.append((item["id"], tag))
return resultatRésumé
- Les compréhensions
[...],{...},{k: v for ...}sont plus rapides et plus lisibles qu'une bouclefor+append. - Les générateurs
(...)sont paresseux : indispensables pour de gros volumes de données. - Le
:=(walrus) évite de recalculer une expression coûteuse dans le filtre.
Exercices pratiques
Mission : sauver un serveur qui sature en mémoire
Objectif : Diagnostiquer une consommation mémoire excessive due à une liste en compréhension, et la remplacer par un générateur.
Contexte
Un service traite un fichier de dix millions de lignes de log avec lignes_erreur = [l for l in toutes_les_lignes if "ERROR" in l] puis calcule immédiatement sum(1 for l in lignes_erreur). Le serveur consomme plusieurs centaines de mégaoctets de RAM rien que pour ce comptage, et l'équipe d'infra menace de couper le service.
Tu dois identifier pourquoi cette écriture consomme autant de mémoire alors qu'elle ne sert qu'à compter, la corriger avec un générateur, puis raisonner sur une compréhension imbriquée trouvée ailleurs dans le code.