Regroupement de listes Python
Apprenez trois façons de regrouper des listes Python : defaultdict, itertools.groupby et les dict comprehensions — avec exemples et pièges.
Regrouper une liste signifie partitionner ses éléments en sous-collections partageant une clé commune — par exemple, regrouper des mots selon leur première lettre, ou regrouper des enregistrements selon un champ de catégorie. Python propose trois approches principales : une boucle manuelle avec collections.defaultdict, itertools.groupby de la bibliothèque standard, et les dict comprehensions. Ce chapitre explique chaque technique, quand choisir l'une plutôt qu'une autre, et les pièges à éviter.
Chapitres connexes : Python Lists · List Methods · List Comprehension · Loop Lists · collections Module
Ce que signifie « regrouper »
Étant donné une liste plate et une fonction de clé qui associe chaque élément à un libellé de groupe, l'objectif est de produire une correspondance entre chaque libellé et la liste des éléments qui lui appartiennent :
['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
key = first letter
→ {'a': ['apple', 'avocado', 'apricot'],
'b': ['banana', 'blueberry'],
'c': ['cherry']}Les trois techniques ci-dessous produisent toutes ce type de résultat. Elles diffèrent par leur verbosité, leurs performances et les contraintes qu'elles imposent sur l'entrée.
Technique 1 : boucle manuelle avec defaultdict
collections.defaultdict est l'approche la plus courante et la plus flexible. Lorsque vous accédez à une clé qui n'existe pas encore, un defaultdict(list) crée automatiquement une liste vide pour cette clé, vous évitant ainsi d'avoir à écrire un test if key in d.
from collections import defaultdict
words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
by_letter = defaultdict(list)
for word in words:
by_letter[word[0]].append(word)
for letter, group in sorted(by_letter.items()):
print(f'{letter}: {group}')
# a: ['apple', 'avocado', 'apricot']
# b: ['banana', 'blueberry']
# c: ['cherry']Pourquoi utiliser defaultdict plutôt qu'un dict ordinaire ?
Avec un dict ordinaire, vous devez effectuer une vérification explicite avant le premier append :
# Plain dict — more boilerplate, same result
by_letter = {}
for word in words:
if word[0] not in by_letter:
by_letter[word[0]] = []
by_letter[word[0]].append(word)Une alternative plus courte avec un dict ordinaire est dict.setdefault :
by_letter = {}
for word in words:
by_letter.setdefault(word[0], []).append(word)setdefault convient pour les scripts courts, mais defaultdict est plus rapide (pas de recherche répétée de clé) et exprime plus clairement l'intention.
Regroupement par une clé calculée
La clé peut être n'importe quelle expression, pas seulement un attribut. Ici, une liste d'entiers est divisée en groupes pairs et impairs :
from collections import defaultdict
numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
by_parity = defaultdict(list)
for n in numbers:
by_parity['even' if n % 2 == 0 else 'odd'].append(n)
print('even:', sorted(by_parity['even'])) # even: [2, 4, 6]
print('odd:', sorted(by_parity['odd'])) # odd: [1, 1, 3, 3, 5, 5, 5, 9]Regroupement d'une liste de dicts
C'est le scénario réel le plus courant — regrouper des lignes de données selon la valeur d'un champ :
from collections import defaultdict
data = [
{'category': 'fruit', 'name': 'apple'},
{'category': 'vegetable', 'name': 'carrot'},
{'category': 'fruit', 'name': 'banana'},
{'category': 'vegetable', 'name': 'broccoli'},
]
grouped = defaultdict(list)
for item in data:
grouped[item['category']].append(item['name'])
for category, names in grouped.items():
print(f'{category}: {names}')
# fruit: ['apple', 'banana']
# vegetable: ['carrot', 'broccoli']Technique 2 : itertools.groupby
itertools.groupby regroupe les éléments consécutifs partageant la même clé. Il est utile lorsque vous devez conserver la structure de longueur de séquence ou lorsque les données sont déjà triées et que vous souhaitez éviter de construire tout le dictionnaire en mémoire (il est paresseux/en flux).
from itertools import groupby
words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
# groupby only groups consecutive elements, so sort first
words_sorted = sorted(words, key=lambda w: w[0])
for letter, group in groupby(words_sorted, key=lambda w: w[0]):
print(f'{letter}: {list(group)}')
# a: ['apple', 'avocado', 'apricot']
# b: ['banana', 'blueberry']
# c: ['cherry']Le piège critique : trier avant groupby
groupby ne regroupe que les éléments consécutifs partageant la même clé. Si l'entrée n'est pas triée selon la clé, vous obtenez plusieurs petits groupes au lieu d'un seul groupe par clé :
from itertools import groupby
# Unsorted input — groupby produces WRONG results
numbers = [1, 1, 2, 3, 3, 1, 2, 2]
for key, group in groupby(numbers):
print(f'{key}: {list(group)}')
# 1: [1, 1] ← first run of 1s
# 2: [2]
# 3: [3, 3]
# 1: [1] ← second run of 1s — NOT merged with the first!
# 2: [2, 2]Triez toujours selon la même fonction de clé avant d'appeler groupby :
numbers_sorted = sorted(numbers)
for key, group in groupby(numbers_sorted):
print(f'{key}: {list(group)}')
# 1: [1, 1, 1]
# 2: [2, 2, 2]
# 3: [3, 3]Quand groupby est avantageux : traitement en flux de données volumineuses
Comme groupby retourne un itérateur, il ne charge pas tous les groupes en mémoire en même temps. Cela le rend utile pour traiter de grands fichiers triés ligne par ligne sans construire un dictionnaire complet.
from itertools import groupby
# Grouping namedtuple records
from collections import namedtuple
Product = namedtuple('Product', ['category', 'name', 'price'])
products = [
Product('dairy', 'milk', 1.10),
Product('fruit', 'apple', 1.20),
Product('fruit', 'banana', 0.50),
Product('vegetable', 'broccoli', 1.50),
Product('vegetable', 'carrot', 0.80),
]
# products is already sorted by category here
for category, group in groupby(products, key=lambda p: p.category):
items = list(group)
print(f'{category}: {[p.name for p in items]}')
# dairy: ['milk']
# fruit: ['apple', 'banana']
# vegetable: ['broccoli', 'carrot']Technique 3 : Dict comprehension
Une dict comprehension construit le dictionnaire groupé en une seule expression. C'est concis, mais cela présente un inconvénient : la list comprehension interne réanalyse l'intégralité de l'entrée pour chaque clé unique, ce qui lui confère une complexité O(n × k) où k est le nombre de clés uniques. Pour les petites listes, c'est acceptable ; pour les grandes listes, préférez defaultdict.
words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
# Collect unique keys first, then build each group
letters = sorted(set(w[0] for w in words))
grouped = {letter: [w for w in words if w[0] == letter] for letter in letters}
for letter, group in grouped.items():
print(f'{letter}: {group}')
# a: ['apple', 'avocado', 'apricot']
# b: ['banana', 'blueberry']
# c: ['cherry']Cette technique est la plus lisible lorsque l'ensemble de clés est petit et déjà connu — par exemple, pour regrouper des résultats True/False ou un ensemble fixe de catégories.
Agréger les groupes après regroupement
Une étape courante après le regroupement est l'agrégation : calculer une somme, une moyenne, un minimum ou un comptage par groupe. Combinez defaultdict(list) avec l'arithmétique Python standard :
from collections import defaultdict
scores = [
('Alice', 90), ('Bob', 75), ('Alice', 85),
('Bob', 88), ('Carol', 92),
]
by_student = defaultdict(list)
for name, score in scores:
by_student[name].append(score)
for student, student_scores in sorted(by_student.items()):
avg = sum(student_scores) / len(student_scores)
print(f'{student}: scores={student_scores}, avg={avg:.1f}')
# Alice: scores=[90, 85], avg=87.5
# Bob: scores=[75, 88], avg=81.5
# Carol: scores=[92], avg=92.0Choisir la bonne technique
| Situation | Meilleur choix |
|---|---|
| Regroupement général, ordre quelconque | defaultdict(list) |
| Besoin de traiter en flux de grandes données triées | itertools.groupby |
| Petite liste, expression concise en une ligne | Dict comprehension |
| L'entrée est déjà triée | defaultdict ou groupby |
| Besoin d'agréger (somme, moyenne, etc.) | defaultdict(list) + arithmétique |
Pièges courants
Oublier de trier avant groupby. groupby ne fusionne que les clés identiques consécutives. Triez toujours l'entrée avec sorted() selon la même fonction de clé avant de la passer à groupby.
Ne pas convertir list(group) immédiatement. L'itérateur de groupe retourné par groupby est épuisé dès que la boucle for externe passe à la clé suivante. Convertissez-le en liste dans le corps de la boucle si vous devez l'utiliser plus d'une fois.
Modifier la liste d'entrée pendant le regroupement. Ajouter ou supprimer des éléments de la liste pendant une boucle de regroupement produit des résultats imprévisibles. Construisez d'abord le dictionnaire groupé, puis modifiez les éléments.
defaultdict apparaissant dans repr. defaultdict(list, {...}) est différent d'un dict ordinaire dans repr. Encapsulez-le avec dict(grouped) lorsque vous avez besoin d'une sortie dict ordinaire.