Une liste Python n’est pas un simple conteneur ordonné. C’est un tableau dynamique d’adresses mémoire, et cette distinction conditionne chaque décision d’architecture dès que le volume de données dépasse le prototype. Nous abordons ici les cas où la liste reste le bon choix, ceux où elle ne l’est plus, et les patterns concrets qui séparent un code de production d’un exercice de tutoriel.
Coût mémoire réel d’une liste Python et seuil de bascule vers un générateur
Chaque élément d’une liste occupe un pointeur (une référence CPython) en plus de l’objet pointé. Sur une liste d’entiers, l’overhead par élément est sensiblement plus élevé que la donnée utile elle-même. Pour quelques milliers d’items, la question ne se pose pas. Au-delà de plusieurs centaines de milliers d’éléments, la consommation mémoire devient un facteur opérationnel.
A voir aussi : Mel ouvert Caen pour les contractuels et remplaçants : créer et activer son accès
Un générateur, créé via une expression génératrice ou une fonction avec yield, ne matérialise jamais la séquence complète. L’expression (x**2 for x in range(1_000_000)) consomme une quantité de mémoire fixe, indépendante du nombre d’itérations. La version liste équivalente alloue la totalité avant le premier parcours.
La règle de décision que nous appliquons en production est directe :
- La séquence sera parcourue plusieurs fois, indexée ou triée : liste
- La séquence est parcourue une seule fois dans un pipeline (lecture de fichier, flux réseau, transformation chaînée) : générateur
- Le volume est inconnu à l’avance ou potentiellement très grand : générateur, puis conversion en liste uniquement si un accès aléatoire est requis en aval

List comprehension en production : patterns fiables et pièges fréquents
La list comprehension n’est pas qu’un raccourci syntaxique. Elle est exécutée dans un scope dédié depuis Python 3, ce qui évite la pollution du namespace local par la variable d’itération. C’est un avantage concret dans les fonctions longues.
Filtrage et transformation en une passe
Le pattern le plus courant en code métier consiste à combiner un filtre et une transformation. Plutôt qu’un for suivi d’un if et d’un append, une comprehension rend l’intention explicite :
valides = [ligne.strip() for ligne in donnees if ligne.strip()]
Ce code supprime les lignes vides et nettoie les espaces en une seule expression. Nous recommandons de limiter la comprehension à une seule condition. Dès qu’un second if ou un else imbriqué apparaît, la lisibilité chute et un bloc for classique devient préférable.
Comprehension imbriquée pour aplatir une matrice
L’aplatissement d’une liste imbriquée revient souvent dans le traitement de données tabulaires ou de résultats d’API paginés. La syntaxe repose sur deux boucles for successives à l’intérieur de la comprehension : la première itère sur chaque sous-liste de la matrice, la seconde sur chaque valeur de cette sous-liste.
L’ordre de lecture suit celui des boucles for équivalentes. Aller au-delà de deux niveaux d’imbrication dans une comprehension produit du code que personne ne relit sans effort. Dans ce cas, itertools.chain.from_iterable est plus lisible et reste paresseux.
Méthodes de liste souvent mal utilisées dans du code réel
append vs extend est une confusion classique qui provoque des bugs silencieux. append ajoute un objet unique (y compris une liste entière comme élément). extend itère sur l’argument et ajoute chaque élément individuellement. Confondre les deux produit des listes imbriquées involontaires détectées parfois très tard dans le pipeline.
sort() trie en place et renvoie None. Écrire resultat = ma_liste.sort() est un piège récurrent : resultat vaut None et la liste originale est modifiée. La fonction sorted() renvoie une nouvelle liste sans toucher à l’originale. Utiliser sorted() quand l’immutabilité de la source compte.
pop() sans argument retire le dernier élément en O(1). Avec un index arbitraire, la complexité passe en O(n) parce que les éléments suivants sont décalés. Pour des suppressions fréquentes en tête de liste, collections.deque offre un O(1) aux deux extrémités.

Parcours de liste Python avec enumerate et unpacking
Parcourir une liste avec un compteur manuel (i = 0 suivi d’un i += 1) est un anti-pattern. La fonction enumerate fournit l’index et la valeur dans une seule boucle for, sans risque de désynchronisation.
for i, element in enumerate(ma_liste, start=1):
Le paramètre start est ignoré dans la majorité des tutoriels. Il permet d’aligner l’index sur une numérotation métier (lignes de fichier commençant à 1, par exemple) sans ajouter d’opération arithmétique dans le corps de la boucle.
L’unpacking s’applique aussi aux listes de tuples ou de listes imbriquées :
for nom, score in resultats:
Ce pattern élimine les accès par index (resultats[i][0], resultats[i][1]) et rend le code auto-documenté. Chaque variable nommée remplace un commentaire.
Quand remplacer une liste par une autre structure de donnees
La liste est le choix par défaut, mais elle n’est pas toujours le bon. Voici les cas de remplacement que nous rencontrons le plus souvent :
- Recherche d’appartenance fréquente : un
setteste l’appartenance en O(1) contre O(n) pour une liste. Dès que le code contient unif x in grande_listedans une boucle, convertir ensetélimine un goulot courant - Accès par clé plutôt que par position : un
dictest plus adapté qu’une liste de tuples parcourue séquentiellement - Séquence de taille fixe et immuable : un
tupleconsomme moins de mémoire et signale l’intention d’immutabilité - Données numériques homogènes en volume : un tableau NumPy stocke les valeurs directement, sans l’overhead de pointeur par élément
Le choix de la structure conditionne la performance et la lisibilité du code bien avant toute optimisation algorithmique. Une liste utilisée là où un set suffirait n’est pas un problème de style, c’est une dette technique mesurable dès que le jeu de données grandit.

