List, dict, set comprehension en Python : code concis et lisible
1. Aperçu
La comprehension est une syntaxe caractéristique de Python. Elle vous permet de créer une liste, un dictionnaire ou un ensemble à partir d’un itérable en une seule ligne. Si bien écrites, elles produisent un code plus court et plus clair qu’une boucle for. Mal utilisées, elles deviennent une seule ligne longue et difficile à comprendre.
Cet article parcourt les list, dict et set comprehensions ainsi que les expressions de générateur avec des exemples concrets. Après la lecture, vous saurez quand utiliser les comprehensions, quand revenir à la boucle for, et comment tester la logique de transformation de données. Tous les exemples s’exécutent sur Python 3.12.
2. List comprehension
2.1. Syntaxe de base
Supposez que vous avez une liste de commandes et que vous devez extraire le montant total de chacune. La façon traditionnelle d’écrire avec une boucle for est la suivante :
from dataclasses import dataclass
@dataclass
class Order:
id: int
customer: str
total: float
status: str
orders = [
Order(1, "An", 250_000, "PAID"),
Order(2, "Bình", 90_000, "CANCELLED"),
Order(3, "An", 1_200_000, "PAID"),
Order(4, "Chi", 450_000, "PENDING"),
]
totals = []
for order in orders:
totals.append(order.total)
print(totals)
Réécrivez avec une list comprehension :
totals = [order.total for order in orders]
print(totals)
[250000, 90000, 1200000, 450000]
La structure est [expression for élément in itérable]. Lisez de gauche à droite : « prenez order.total pour chaque order dans orders ». Vous n’avez pas besoin de déclarer une liste vide ni d’appeler append. L’intention « je crée une nouvelle liste » est évidente dès le [.
2.2. Filtrer des éléments avec if
Si vous ne voulez que les commandes payées, vous ajoutez une clause if à la fin :
paid_ids = [order.id for order in orders if order.status == "PAID"]
print(paid_ids)
[1, 3]
Le if à la fin joue le rôle d’un filtre. Les éléments qui ne satisfont pas la condition sont ignorés, donc la liste résultante peut être plus courte que la liste d’origine.
2.3. Expression if-else au début
Quand vous voulez transformer chaque élément selon une condition au lieu de filtrer, vous mettez if-else dans la partie expression :
labels = ["grand" if order.total >= 500_000 else "petit" for order in orders]
print(labels)
['petit', 'petit', 'grand', 'petit']
C’est là que beaucoup de gens se trompent. Vous avez juste besoin de vous souvenir de deux règles :
- Le
ifaprèsforest utilisé pour filtrer. Cette clause n’a pas d’else. a if cond else bavantforest une expression conditionnelle. Cette clause doit avoir unelseet conserve le nombre d’éléments.
Écrire [x for x in data if cond else y] causera une SyntaxError.
3. Dict comprehension
La dict comprehension utilise {} avec des paires clé : valeur. Par exemple, vous devez rechercher rapidement une commande par son id :
orders_by_id = {order.id: order for order in orders}
print(orders_by_id[3])
Order(id=3, customer='An', total=1200000, status='PAID')
Vous pouvez également transformer un dict existant. Par exemple, appliquer une taxe de 10 % à une liste de prix :
prices = {"café": 30_000, "thé_lait": 45_000, "sandwich": 20_000}
prices_with_vat = {name: round(price * 1.1) for name, price in prices.items()}
print(prices_with_vat)
{'café': 33000, 'thé_lait': 49500, 'sandwich': 22000}
prices.items() retourne les paires (clé, valeur), et vous les déballez directement dans la boucle for.
Une astuce utile est d’inverser un dict :
status_codes = {"PAID": 1, "PENDING": 2, "CANCELLED": 3}
code_to_status = {code: status for status, code in status_codes.items()}
print(code_to_status[2])
PENDING
Remarque : Si deux éléments produisent la même clé, l’élément suivant écrase silencieusement le précédent. Par exemple,
{order.customer: order.total for order in orders}ne conserve que la dernière commande de « An » (1.200.000), et la commande de 250.000 disparaît sans avertissement. Quand vous avez besoin de regrouper, utilisezdefaultdictavec une bouclefor(voir section 7).
4. Set comprehension
La set comprehension utilise aussi {} mais avec une seule expression, sans :. Le résultat supprime automatiquement les doublons. Par exemple, vous voulez savoir quels clients ont commandé :
customers = {order.customer for order in orders}
print(sorted(customers))
['An', 'Bình', 'Chi']
Chaque client n’apparaît qu’une fois, bien que « An » ait deux commandes. L’ordre dans un set n’est pas garanti, donc l’exemple ci-dessus utilise sorted lors de l’affichage.
La set comprehension est très pratique pour vérifier rapidement l’appartenance :
banned_emails = {"spam@x.com", "Fake@Y.com"}
normalized = {email.strip().lower() for email in banned_emails}
print("fake@y.com" in normalized)
True
Remarque :
{}vide est un dict, pas un set. Pour créer un set vide, vous devez écrireset().
5. Plusieurs boucles for et comprehensions imbriquées
Vous pouvez écrire plusieurs for dans une seule comprehension. L’ordre de lecture est exactement le même que quand vous écrivez des boucles for imbriquées de haut en bas :
carts = {
"An": ["café", "sandwich"],
"Chi": ["thé_lait"],
}
items = [(customer, item) for customer, products in carts.items() for item in products]
print(items)
[('An', 'café'), ('An', 'sandwich'), ('Chi', 'thé_lait')]
La comprehension ci-dessus est équivalente à ce code :
items = []
for customer, products in carts.items():
for item in products:
items.append((customer, item))
Le cas le plus courant est d’aplatir (flatten) une liste imbriquée :
matrix = [[1, 2, 3], [4, 5, 6]]
flat = [value for row in matrix for value in row]
print(flat)
[1, 2, 3, 4, 5, 6]
Moyen de vous en souvenir : la boucle for la plus externe vient en premier. Si vous devez vous arrêter et relire pour comprendre l’ordre, c’est un signe qu’il faut la diviser en boucles for ordinaires.
6. Expression de générateur : quand vous n’avez pas besoin de la liste
Si vous remplacez [] par (), vous obtenez une expression de générateur. Python ne crée pas la liste en mémoire. Les valeurs sont générées une par une au fur et à mesure qu’un code les consomme.
revenue = sum(order.total for order in orders if order.status == "PAID")
print(revenue)
1450000
Quand le générateur est l’argument unique d’une fonction, vous pouvez omettre une paire de parenthèses. C’est approprié pour sum, any, all, max, min :
has_big_order = any(order.total > 1_000_000 for order in orders)
all_positive = all(order.total > 0 for order in orders)
print(has_big_order, all_positive)
True True
any s’arrête dès qu’il rencontre le premier élément qui satisfait la condition. Ainsi, avec de grandes données, un générateur économise la mémoire et est plus rapide que any([...]).
Remarque : Un générateur ne peut être parcouru qu’une seule fois. Le deuxième passage ne retournera rien :
paid = (o for o in orders if o.status == "PAID")
print(len(list(paid)))
print(len(list(paid)))
2
0
Si vous devez utiliser le résultat plusieurs fois, utilisez une list comprehension.
7. Garder les comprehensions lisibles
7.1. Donner un nom de fonction à la logique complexe
Les conditions longues rendent les comprehensions difficiles à lire :
vip = [o.customer for o in orders if o.status == "PAID" and o.total >= 1_000_000 and not o.customer.startswith("test")]
Vous pouvez extraire la condition dans une fonction bien nommée :
def is_vip_order(order: Order) -> bool:
return (
order.status == "PAID"
and order.total >= 1_000_000
and not order.customer.startswith("test")
)
vip = [order.customer for order in orders if is_vip_order(order)]
print(vip)
['An']
Maintenant, la comprehension se lit comme une phrase, et la logique métier a sa propre place pour être testée.
7.2. Sauts de ligne raisonnables
Une comprehension ne doit pas nécessairement tenir sur une seule ligne. Entre parenthèses, vous pouvez progresser librement :
summary = {
order.id: order.total
for order in orders
if order.status != "CANCELLED"
}
print(summary)
{1: 250000, 3: 1200000, 4: 450000}
Chaque ligne a un rôle : l’expression, la source, le filtre.
7.3. Opérateur walrus pour éviter de calculer deux fois
Parfois, vous avez besoin de filtrer et d’utiliser le résultat du même calcul. L’opérateur := (Python 3.8+) vous permet de ne le calculer qu’une seule fois :
def discount_for(order: Order) -> float:
return order.total * 0.05 if order.total >= 400_000 else 0
discounts = {
order.id: d
for order in orders
if (d := discount_for(order)) > 0
}
print(discounts)
{3: 60000.0, 4: 22500.0}
Utilisez l’opérateur walrus avec parcimonie. La variable d « s’échappera » de la portée du comprehension. De plus, beaucoup de lecteurs ne sont pas familiers avec cette syntaxe.
8. Tester la logique de transformation de données
Les comprehensions se trouvent généralement dans des fonctions de transformation de données, vous devez donc les tester comme n’importe quelle autre logique. Tout d’abord, installez pytest :
pip install pytest
Supposez que vous ayez un module reports.py :
# reports.py
from dataclasses import dataclass
@dataclass
class Order:
id: int
customer: str
total: float
status: str
def revenue_by_customer(orders: list[Order]) -> dict[str, float]:
customers = {o.customer for o in orders if o.status == "PAID"}
return {
c: sum(o.total for o in orders if o.customer == c and o.status == "PAID")
for c in customers
}
Et le fichier test correspondant :
# test_reports.py
from reports import Order, revenue_by_customer
def test_revenue_only_counts_paid_orders():
orders = [
Order(1, "An", 250_000, "PAID"),
Order(2, "An", 1_200_000, "PAID"),
Order(3, "Bình", 90_000, "CANCELLED"),
Order(4, "Chi", 450_000, "PAID"),
]
result = revenue_by_customer(orders)
assert result == {"An": 1_450_000, "Chi": 450_000}
assert "Bình" not in result
def test_empty_orders_returns_empty_dict():
assert revenue_by_customer([]) == {}
pytest -q
.. [100%]
2 passed in 0.01s
Le premier test affirme deux choses : les commandes annulées ne sont pas comptées, et les clients qui n’ont que des commandes annulées n’apparaissent pas dans le résultat. Le deuxième test vérifie le cas limite avec une liste vide.
La fonction ci-dessus est correcte mais a une complexité O(n × nombre de clients) car pour chaque client, on parcourt toutes les orders. Avec de grandes données, une boucle for avec defaultdict est à la fois plus rapide et plus lisible :
from collections import defaultdict
def revenue_by_customer(orders: list[Order]) -> dict[str, float]:
totals: dict[str, float] = defaultdict(float)
for order in orders:
if order.status == "PAID":
totals[order.customer] += order.total
return dict(totals)
Les deux anciens tests passent toujours sans modification. C’est justement l’avantage d’avoir des tests : vous pouvez refactoriser en toute confiance entre les comprehensions et les boucles for.
9. Quand NE PAS utiliser les comprehensions
- Quand vous avez besoin d’effets secondaires. N’écrivez pas
[print(o) for o in orders]ou[db.save(o) for o in orders]. Cela crée une listeNoneinutile et trompe le lecteur en lui faisant croire que vous avez besoin du résultat. Utilisez une bouclefor. - Quand plus de deux niveaux d’imbrication ou plusieurs
ifentrelacés. Trois bouclesforsur une seule ligne sont presque toujours moins lisibles qu’une version éclatée. - Quand vous devez regrouper ou accumuler, comme faire des totaux ou compter. Utilisez
defaultdict,Counterou une bouclefor. - Quand vous devez gérer les exceptions pour chaque élément. Les comprehensions ne peuvent pas contenir
try/except. Vous devez extraire une fonction séparée et l’appeler dans la comprehension, ou utiliser une bouclefor. - Quand vous avez de très grandes données et vous ne la parcourez qu’une seule fois. Utilisez une expression de générateur à la place d’une liste.
Remarque : En Python 3, la variable de boucle d’une comprehension ne s’échappe pas. Après
[x for x in range(3)], le nomxn’existe pas en dehors de la portée, contrairement à une boucleforordinaire. La seule exception est une variable assignée avec:=comme mentionné à la section 7.3.
10. Conclusion
Les comprehensions vous aident à exprimer l’idée « créer une nouvelle collection à partir d’une ancienne » de manière concise :
[...]pour les listes,{k: v ...}pour les dicts,{...}pour les sets, et(...)pour les générateurs.- Le
ifaprèsforsert à filtrer, tandis quea if cond else bavantforsert à transformer. - Extrayez les conditions complexes dans des fonctions bien nommées, et sautez les lignes quand l’expression est longue.
- Utilisez les expressions de générateur avec
sum,any,allpour économiser la mémoire. - Revenez à la boucle
forquand vous avez des effets secondaires, quand vous avez besoin de regrouper, de gérer les exceptions, ou quand la comprehension commence à devenir difficile à lire.
La règle la plus simple : si vos collègues doivent lire votre comprehension deux fois pour la comprendre, réécrivez-la avec une boucle for. Le code concis n’a de valeur que s’il reste lisible.