Cours de Python / Compter et regrouper avec collections et itertools

Le module collections propose des conteneurs spécialisés pour compter ou accumuler des valeurs ; itertools permet de combiner des parcours. Choisissez ces outils selon le sens du résultat, l’ordre des données et la consommation des itérateurs.

Distinguer occurrences et sommes

Counter compte ici les occurrences des catégories : deux lignes food et une ligne bus. Il ne calcule pas leur coût. defaultdict(int) conserve au contraire un total par catégorie ; lors d’un premier accès par crochets à une clé absente, int est appelé sans argument pour fournir zéro, puis le montant est ajouté.

Cette création de clé fait partie du comportement de defaultdict. Sur un dictionnaire vide de ce type, totals.get("food") renvoie None sans insérer de clé, tandis que totals["food"] crée l’entrée avec zéro. Pour une lecture qui ne doit pas modifier le conteneur, get ou un dictionnaire ordinaire peut être plus clair. La documentation de defaultdict précise le rôle de la fabrique.

Counter reçoit dans le programme une expression génératrice qui extrait seulement category. Le nom _ reçoit le montant non utilisé pour ce comptage. Dans l’accumulation, les deux valeurs sont nécessaires. Cette distinction évite de confondre « deux dépenses alimentaires » et « 11500 de dépenses alimentaires ».

Exécuter comptage, total et parcours groupé

Enregistrez le programme complet en UTF-8 dans collections_demo.py, dans un dossier d’exercice. Les deux modules appartiennent à la bibliothèque standard. Depuis ce dossier, lancez python collections_demo.py avec l’environnement virtuel du projet sélectionné. Utilisez python3 collections_demo.py si python3 est votre commande hors environnement virtuel sous macOS ou Ubuntu.

from collections import Counter, defaultdict
from itertools import chain, groupby

rows = [("food", 3500), ("bus", 2500), ("food", 8000)]
counts = Counter(category for category, _ in rows)
totals = defaultdict(int)
for category, amount in rows:
    totals[category] += amount
print(sorted(counts.items()))
print(sorted(totals.items()))
print(list(chain([1, 2], [3])))
for category, group in groupby(sorted(rows), key=lambda row: row[0]):
    print(category, sum(amount for _, amount in group))

Résultat attendu :

[('bus', 1), ('food', 2)]
[('bus', 2500), ('food', 11500)]
[1, 2, 3]
bus 2500
food 11500

Les deux premières lignes sont triées pour rendre leur ordre visible et stable dans cet exercice. Le tri change la présentation, pas la signification des totaux. La catégorie food, « alimentation », contient 3500 et 8000, soit 11500 ; bus contient 2500. chain lit ensuite les éléments de la première liste puis ceux de la seconde : il ne trie pas et ne supprime pas les doublons.

La dernière boucle trie les paires avant groupby. Ici, sorted(rows) place d’abord les catégories dans l’ordre et, à catégorie identique, compare les montants. C’est suffisant pour les données de cet exemple. Lorsque votre règle porte sur une clé particulière, exprimez le même key pour le tri et pour le regroupement, comme dans le programme suivant.

Observer les groupes consécutifs avant et après tri

Enregistrez le deuxième programme dans groups.py, sans remplacer le premier, puis lancez python groups.py dans le même dossier, ou la commande python3 correspondante.

from itertools import groupby
rows = [("food", 3500), ("bus", 2500), ("food", 8000)]
def category(row):
    return row[0]
for label, data in [("original", rows), ("sorted", sorted(rows, key=category))]:
    print(label)
    for name, group in groupby(data, key=category):
        print(name, sum(amount for _, amount in group))
print(category(rows[0]) == (lambda row: row[0])(rows[0]))

Résultat attendu :

original
food 3500
bus 2500
food 8000
sorted
bus 2500
food 11500
True

Dans original, food apparaît, puis bus, puis food à nouveau : groupby produit donc trois groupes. Après le tri selon category, les lignes food deviennent voisines et leur somme est calculée en une fois. À la différence du GROUP BY de SQL, cet outil ne rassemble pas de lui-même toutes les clés égales dispersées dans la source.

Le paramètre key est une fonction qui extrait le critère de regroupement. lambda row: row[0] et la fonction category renvoient ici le premier élément de la paire. La dernière ligne True compare leur résultat sur la première ligne, pas l’identité des fonctions. Le contrat de groupby décrit les groupes consécutifs et leur source partagée.

Chaque group est un itérateur qui partage le parcours sous-jacent. Consommez-le avant de demander le groupe suivant, comme le fait sum dans la boucle. Si vous devez relire ses lignes plus tard, construisez list(group) à ce moment et conservez cette liste. Stocker seulement les objets group puis avancer la boucle ne conserve pas une copie de leurs données. Le cours sur les itérateurs et générateurs explique ce modèle de consommation.

Contrôler l’ordre, les entrées vides et la mémoire

  1. Dans collections_demo.py, remplacez seulement groupby(sorted(rows), par groupby(rows,. Les compteurs et les totaux du début restent identiques, mais la dernière boucle affiche food 3500, bus 2500, puis food 8000.
  2. Rétablissez sorted(rows) et vérifiez le retour à deux groupes, bus 2500 et food 11500.
  3. Dans ce même fichier, remplacez temporairement rows par []. Les compteurs et totaux affichent deux listes vides ; chain affiche encore [1, 2, 3] et la boucle de groupes n’affiche rien. Rétablissez les trois lignes.

Pour obtenir des sommes globales sans tenir compte de l’ordre d’arrivée, l’accumulation par dictionnaire est souvent directe. groupby convient également à des séquences dont les changements de clé ont un sens, ou à une source déjà organisée par clé. Trier toute la source demande de la collecter et peut changer l’ordre métier : ce choix n’est pas gratuit pour un grand flux.

chain parcourt successivement ses sources sans fabriquer lui-même la liste finale ; list(chain(...)) stocke tous les éléments produits. Pour limiter un parcours, islice peut sélectionner un nombre fini d’éléments. Par exemple, après import de count et islice depuis itertools, list(islice(count(1), 3)) donne [1, 2, 3]. Ne convertissez pas directement un itérateur infini en liste.

Ces exemples supposent des catégories comparables pour le tri et des montants additionnables. Ils ne valident pas le sens métier des valeurs. Vérifiez les entrées avant l’agrégation si votre application doit refuser certains types ou montants, puis choisissez un compteur ou une somme en fonction de la question posée.