Skip to content

Module collections de Python : Guide Counter, defaultdict, deque, namedtuple

Updated on

Les structures de données intégrées de Python — listes, dicts, tuples, sets — couvrent la plupart des besoins. Mais dès que votre code dépasse des exemples jouets, vous commencez à en voir les limites. Compter des éléments exige des boucles manuelles sur un dictionnaire. Regrouper des données revient à parsemer le code de tests if key not in dict. Utiliser une liste comme file vous « punit » avec des pop en O(n) depuis le début. Représenter des enregistrements structurés avec de simples tuples transforme l’accès aux champs en un jeu de devinettes illisible basé sur des indices. Chaque contournement semble mineur isolément, mais ils s’accumulent vite : le code devient moins lisible, plus lent, et plus fragile.

Le module collections de la bibliothèque standard Python résout ces problèmes grâce à des types de conteneurs conçus pour des usages précis. Counter compte des éléments en un seul appel. defaultdict élimine les KeyError via des valeurs par défaut automatiques. deque offre des opérations en O(1) aux deux extrémités d’une séquence. namedtuple ajoute des noms de champs aux tuples sans le surcoût d’une classe complète. OrderedDict et ChainMap gèrent respectivement l’ordre et les schémas de recherche en couches qu’un dict standard n’exprime pas aussi proprement.

Ce guide couvre chaque classe majeure du module collections avec du code fonctionnel, une analyse de performance, et des patterns réalistes. Que vous traitiez des fichiers de logs, construisiez des caches, gériez des couches de configuration, ou structuriez des pipelines de données, ces conteneurs rendront votre code plus court, plus rapide et plus correct.

📚

Vue d’ensemble du module collections

Le module collections fournit des types de conteneurs spécialisés qui étendent les conteneurs intégrés généralistes de Python.

import collections
 
# See all available classes
print([name for name in dir(collections) if not name.startswith('_')])
# ['ChainMap', 'Counter', 'OrderedDict', 'UserDict', 'UserList',
#  'UserString', 'abc', 'defaultdict', 'deque', 'namedtuple']
ClasseObjectifRemplace
CounterCompter des objets hashablesBoucles manuelles de comptage via dict
defaultdictDict avec valeurs par défaut automatiquesdict.setdefault(), tests if key not in
dequeFile double extrémité avec extrémités en O(1)list utilisée comme file/pile
namedtupleTuple avec champs nommésTuples simples, data classes basiques
OrderedDictDict qui mémorise l’ordre d’insertiondict (avant 3.7), opérations ordonnées
ChainMapRecherches sur dictionnaires en couchesFusion manuelle de dicts

Counter : compter des éléments

Counter est une sous-classe de dict destinée au comptage d’objets hashables. Il associe chaque élément à son nombre d’occurrences et fournit des méthodes d’analyse de fréquence.

Créer un Counter

from collections import Counter
 
# From an iterable
words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
word_count = Counter(words)
print(word_count)
# Counter({'apple': 3, 'banana': 2, 'cherry': 1})
 
# From a string
letter_count = Counter('mississippi')
print(letter_count)
# Counter({'s': 4, 'i': 4, 'p': 2, 'm': 1})
 
# From a dictionary
inventory = Counter({'shirts': 25, 'pants': 15, 'hats': 10})
 
# From keyword arguments
stock = Counter(laptops=5, monitors=12)

most_common() et classement par fréquence

from collections import Counter
 
text = "to be or not to be that is the question"
words = Counter(text.split())
 
# Get the 3 most common words
print(words.most_common(3))
# [('to', 2), ('be', 2), ('or', 1)]
 
# Get all elements sorted by frequency
print(words.most_common())
# [('to', 2), ('be', 2), ('or', 1), ('not', 1), ('that', 1), ('is', 1), ('the', 1), ('question', 1)]
 
# Least common: reverse the list or slice from the end
print(words.most_common()[-3:])
# [('is', 1), ('the', 1), ('question', 1)]

Arithmétique sur Counter

Les counters supportent l’addition, la soustraction, l’intersection et l’union — en les traitant comme des multisets.

from collections import Counter
 
a = Counter(x=4, y=2, z=1)
b = Counter(x=1, y=3, z=5)
 
# Addition: combine counts
print(a + b)  # Counter({'z': 6, 'y': 5, 'x': 5})
 
# Subtraction: drops zero and negative results
print(a - b)  # Counter({'x': 3})
 
# Intersection (min of each)
print(a & b)  # Counter({'y': 2, 'x': 1, 'z': 1})
 
# Union (max of each)
print(a | b)  # Counter({'z': 5, 'x': 4, 'y': 3})

Patterns pratiques avec Counter

from collections import Counter
 
# Word frequency analysis
log_entries = [
    "ERROR: disk full",
    "WARNING: high memory",
    "ERROR: disk full",
    "ERROR: timeout",
    "WARNING: high memory",
    "ERROR: disk full",
    "INFO: backup complete",
]
error_types = Counter(entry.split(":")[0].strip() for entry in log_entries)
print(error_types)
# Counter({'ERROR': 4, 'WARNING': 2, 'INFO': 1})
 
# Find unique elements (count == 1)
data = [1, 2, 3, 2, 1, 4, 5, 4]
unique = [item for item, count in Counter(data).items() if count == 1]
print(unique)  # [3, 5]
 
# Check if one collection is a subset of another (anagram check)
def is_anagram(word1, word2):
    return Counter(word1.lower()) == Counter(word2.lower())
 
print(is_anagram("listen", "silent"))  # True
print(is_anagram("hello", "world"))    # False

Pour un approfondissement de Counter, consultez notre Python Counter guide.

defaultdict : valeurs par défaut automatiques

defaultdict est une sous-classe de dict qui appelle une factory function pour fournir des valeurs par défaut aux clés manquantes, ce qui élimine les KeyError et la nécessité de checks défensifs.

Fonctions de fabrique (factory functions)

from collections import defaultdict
 
# int factory: default is 0
counter = defaultdict(int)
counter['apples'] += 1
counter['oranges'] += 3
print(dict(counter))  # {'apples': 1, 'oranges': 3}
 
# list factory: default is []
groups = defaultdict(list)
pairs = [('fruit', 'apple'), ('veggie', 'carrot'), ('fruit', 'banana'), ('veggie', 'pea')]
for category, item in pairs:
    groups[category].append(item)
print(dict(groups))
# {'fruit': ['apple', 'banana'], 'veggie': ['carrot', 'pea']}
 
# set factory: default is set()
index = defaultdict(set)
words = [('file1', 'python'), ('file2', 'python'), ('file1', 'java'), ('file3', 'python')]
for filename, lang in words:
    index[lang].add(filename)
print(dict(index))
# {'python': {'file1', 'file2', 'file3'}, 'java': {'file1'}}

Le pattern de regroupement (grouping)

Regrouper des données liées est l’usage le plus courant de defaultdict(list). Comparez l’approche manuelle :

from collections import defaultdict
 
students = [
    ('Math', 'Alice'), ('Science', 'Bob'), ('Math', 'Charlie'),
    ('Science', 'Diana'), ('Math', 'Eve'), ('History', 'Frank'),
]
 
# Without defaultdict -- verbose and error-prone
groups_manual = {}
for subject, name in students:
    if subject not in groups_manual:
        groups_manual[subject] = []
    groups_manual[subject].append(name)
 
# With defaultdict -- clean and direct
groups = defaultdict(list)
for subject, name in students:
    groups[subject].append(name)
 
print(dict(groups))
# {'Math': ['Alice', 'Charlie', 'Eve'], 'Science': ['Bob', 'Diana'], 'History': ['Frank']}

defaultdict imbriqués (nested defaultdict)

Construisez des structures multi-niveaux sans initialiser manuellement chaque niveau.

from collections import defaultdict
 
# Two-level nested defaultdict
def nested_dict():
    return defaultdict(int)
 
sales = defaultdict(nested_dict)
sales['2025']['Q1'] = 150000
sales['2025']['Q2'] = 175000
sales['2026']['Q1'] = 200000
print(sales['2025']['Q1'])  # 150000
print(sales['2024']['Q3'])  # 0 (auto-created, no KeyError)
 
# Arbitrary depth nesting with a recursive factory
def deep_dict():
    return defaultdict(deep_dict)
 
config = deep_dict()
config['database']['primary']['host'] = 'localhost'
config['database']['primary']['port'] = 5432
config['database']['replica']['host'] = 'replica.local'
print(config['database']['primary']['host'])  # localhost

Fonctions de fabrique personnalisées

from collections import defaultdict
 
# Lambda for custom defaults
scores = defaultdict(lambda: 100)  # Every student starts with 100
scores['Alice'] -= 5
scores['Bob'] -= 10
print(scores['Charlie'])  # 100 (new student gets default)
print(dict(scores))  # {'Alice': 95, 'Bob': 90, 'Charlie': 100}
 
# Named function for complex defaults
def default_user():
    return {'role': 'viewer', 'active': True, 'login_count': 0}
 
users = defaultdict(default_user)
users['alice']['role'] = 'admin'
print(users['bob'])  # {'role': 'viewer', 'active': True, 'login_count': 0}

Pour plus de patterns, consultez notre Python defaultdict guide.

deque : file double extrémité

deque (prononcé « deck ») fournit des opérations append et pop en O(1) aux deux extrémités. Les listes sont en O(n) pour pop(0) et insert(0, x) car tous les éléments doivent être décalés. Pour toute charge de travail qui touche les deux extrémités d’une séquence, deque est le bon choix.

Opérations de base

from collections import deque
 
d = deque([1, 2, 3, 4, 5])
 
# O(1) operations on both ends
d.append(6)         # Add to right: [1, 2, 3, 4, 5, 6]
d.appendleft(0)     # Add to left:  [0, 1, 2, 3, 4, 5, 6]
 
right = d.pop()     # Remove from right: 6
left = d.popleft()  # Remove from left:  0
print(d)  # deque([1, 2, 3, 4, 5])
 
# Extend from both sides
d.extend([6, 7])          # Right extend: [1, 2, 3, 4, 5, 6, 7]
d.extendleft([-1, 0])     # Left extend (reversed): [0, -1, 1, 2, 3, 4, 5, 6, 7]

Deques bornées avec maxlen

Quand maxlen est défini, ajouter des éléments au-delà de la limite supprime automatiquement des éléments depuis l’extrémité opposée. C’est parfait pour les sliding windows et les caches.

from collections import deque
 
# Keep only the last 5 items
recent = deque(maxlen=5)
for i in range(10):
    recent.append(i)
 
print(recent)  # deque([5, 6, 7, 8, 9], maxlen=5)
 
# Sliding window average
def moving_average(iterable, window_size):
    window = deque(maxlen=window_size)
    for value in iterable:
        window.append(value)
        if len(window) == window_size:
            yield sum(window) / window_size
 
data = [10, 20, 30, 40, 50, 60, 70]
print(list(moving_average(data, 3)))
# [20.0, 30.0, 40.0, 50.0, 60.0]

Rotation

rotate(n) décale les éléments de n positions vers la droite. Les valeurs négatives font une rotation vers la gauche.

from collections import deque
 
d = deque([1, 2, 3, 4, 5])
 
d.rotate(2)   # Rotate right by 2
print(d)  # deque([4, 5, 1, 2, 3])
 
d.rotate(-3)  # Rotate left by 3
print(d)  # deque([2, 3, 4, 5, 1])

Performances : deque vs list

from collections import deque
import time
 
# Benchmark: append/pop from left side
n = 100_000
 
# List: O(n) for each insert at position 0
start = time.perf_counter()
lst = []
for i in range(n):
    lst.insert(0, i)
list_time = time.perf_counter() - start
 
# Deque: O(1) for appendleft
start = time.perf_counter()
dq = deque()
for i in range(n):
    dq.appendleft(i)
deque_time = time.perf_counter() - start
 
print(f"List insert(0, x): {list_time:.4f}s")
print(f"Deque appendleft:  {deque_time:.4f}s")
print(f"Deque is {list_time / deque_time:.0f}x faster")
# Typical output:
# List insert(0, x): 1.2340s
# Deque appendleft:  0.0065s
# Deque is 190x faster
Opérationlistdeque
append(x) (à droite)O(1) amortiO(1)
pop() (à droite)O(1)O(1)
insert(0, x) / appendleft(x)O(n)O(1)
pop(0) / popleft()O(n)O(1)
access by index [i]O(1)O(n)
Mémoire par élémentPlus faibleLégèrement plus élevée

Utilisez deque quand vous avez besoin d’opérations rapides aux deux extrémités. Utilisez list quand vous avez besoin d’un accès aléatoire rapide par indice.

Pour le guide complet, voir Python deque.

namedtuple : tuples avec champs nommés

namedtuple crée des sous-classes de tuple avec des champs nommés, rendant le code auto-documenté sans le surcoût de définir une classe complète.

Créer des namedtuples

from collections import namedtuple
 
# Define a type
Point = namedtuple('Point', ['x', 'y'])
p = Point(3, 4)
 
# Access by name or index
print(p.x)     # 3
print(p[1])    # 4
print(p)       # Point(x=3, y=4)
 
# Alternative field definition styles
Color = namedtuple('Color', 'red green blue')        # Space-separated string
Config = namedtuple('Config', 'host, port, database')  # Comma-separated string

Pourquoi utiliser namedtuple plutôt que des tuples simples ?

from collections import namedtuple
 
# Plain tuple: which index is what?
employee_tuple = ('Alice', 'Engineering', 95000, True)
print(employee_tuple[2])  # 95000 -- but what does index 2 mean?
 
# namedtuple: self-documenting
Employee = namedtuple('Employee', 'name department salary active')
employee = Employee('Alice', 'Engineering', 95000, True)
print(employee.salary)     # 95000 -- immediately clear
print(employee.department) # Engineering

Méthodes clés

from collections import namedtuple
 
Employee = namedtuple('Employee', 'name department salary')
emp = Employee('Alice', 'Engineering', 95000)
 
# _replace: create a new instance with some fields changed (immutable)
promoted = emp._replace(salary=110000)
print(promoted)  # Employee(name='Alice', department='Engineering', salary=110000)
print(emp)       # Employee(name='Alice', department='Engineering', salary=95000)  -- unchanged
 
# _asdict: convert to OrderedDict (Python 3.8+ returns regular dict)
print(emp._asdict())
# {'name': 'Alice', 'department': 'Engineering', 'salary': 95000}
 
# _fields: get field names
print(Employee._fields)  # ('name', 'department', 'salary')
 
# _make: create from an iterable
data = ['Bob', 'Marketing', 85000]
emp2 = Employee._make(data)
print(emp2)  # Employee(name='Bob', department='Marketing', salary=85000)

Valeurs par défaut

from collections import namedtuple
 
# defaults parameter (Python 3.6.1+)
Connection = namedtuple('Connection', 'host port timeout', defaults=[5432, 30])
conn1 = Connection('localhost')               # port=5432, timeout=30
conn2 = Connection('db.example.com', 3306)    # timeout=30
conn3 = Connection('db.example.com', 3306, 60)
 
print(conn1)  # Connection(host='localhost', port=5432, timeout=30)
print(conn2)  # Connection(host='db.example.com', port=3306, timeout=30)

Alternative : typing.NamedTuple

Pour les annotations de type et une syntaxe plus proche d’une classe, utilisez typing.NamedTuple :

from typing import NamedTuple
 
class Point(NamedTuple):
    x: float
    y: float
    label: str = "origin"
 
p = Point(3.0, 4.0, "A")
print(p.x, p.label)  # 3.0 A
 
# Still a tuple -- supports unpacking, indexing, iteration
x, y, label = p
print(f"({x}, {y})")  # (3.0, 4.0)

namedtuple vs dataclass

Fonctionnaliténamedtupledataclass
Immutable par défautOuiNon (frozen=True requis)
Empreinte mémoireIdentique à tuple (faible)Plus élevée (classe standard)
Itération/dépaquetageOui (c’est un tuple)Non (sauf ajout de méthodes)
Annotations de typeVia typing.NamedTupleNatif
Méthodes/propriétésNécessite de sous-classerSupport direct
HéritageLimitéHéritage complet de classe
Idéal pourEnregistrements de données légersObjets mutables complexes

OrderedDict : opérations de dictionnaire ordonné

Depuis Python 3.7, un dict standard préserve l’ordre d’insertion. Alors, quand avez-vous encore besoin de OrderedDict ?

Quand OrderedDict reste utile

from collections import OrderedDict
 
# 1. Equality considers order
d1 = {'a': 1, 'b': 2}
d2 = {'b': 2, 'a': 1}
print(d1 == d2)  # True -- regular dicts ignore order in comparison
 
od1 = OrderedDict([('a', 1), ('b', 2)])
od2 = OrderedDict([('b', 2), ('a', 1)])
print(od1 == od2)  # False -- OrderedDict considers order
 
# 2. move_to_end() for reordering
od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
od.move_to_end('a')           # Move 'a' to the end
print(list(od.keys()))  # ['b', 'c', 'a']
 
od.move_to_end('c', last=False)  # Move 'c' to the beginning
print(list(od.keys()))  # ['c', 'b', 'a']

Construire un cache LRU avec OrderedDict

from collections import OrderedDict
 
class LRUCache:
    def __init__(self, capacity):
        self.cache = OrderedDict()
        self.capacity = capacity
 
    def get(self, key):
        if key not in self.cache:
            return -1
        self.cache.move_to_end(key)  # Mark as recently used
        return self.cache[key]
 
    def put(self, key, value):
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            self.cache.popitem(last=False)  # Remove oldest
 
cache = LRUCache(3)
cache.put('a', 1)
cache.put('b', 2)
cache.put('c', 3)
cache.get('a')       # Access 'a', moves it to end
cache.put('d', 4)    # Evicts 'b' (least recently used)
print(list(cache.cache.keys()))  # ['c', 'a', 'd']

ChainMap : recherches de dictionnaires en couches

ChainMap regroupe plusieurs dictionnaires en une vue unique pour les recherches. Il parcourt chaque dictionnaire dans l’ordre et renvoie la première correspondance. C’est idéal pour la superposition de configurations, les recherches de variables par portée, et la gestion de contexte.

Utilisation de base

from collections import ChainMap
 
defaults = {'theme': 'light', 'language': 'en', 'timeout': 30}
user_prefs = {'theme': 'dark'}
session = {'language': 'fr'}
 
config = ChainMap(session, user_prefs, defaults)
 
# Lookup searches session -> user_prefs -> defaults
print(config['theme'])     # 'dark'    (from user_prefs)
print(config['language'])  # 'fr'      (from session)
print(config['timeout'])   # 30        (from defaults)

Superposition (layering) de configuration

from collections import ChainMap
import os
 
# Real-world config pattern: CLI args > env vars > config file > defaults
defaults = {
    'debug': False,
    'log_level': 'WARNING',
    'port': 8080,
    'host': '0.0.0.0',
}
 
config_file = {
    'log_level': 'INFO',
    'port': 9090,
}
 
env_vars = {
    k.lower(): v for k, v in os.environ.items()
    if k.lower() in defaults
}
 
cli_args = {'debug': True}  # Parsed from argparse
 
config = ChainMap(cli_args, env_vars, config_file, defaults)
print(config['debug'])      # True (from cli_args)
print(config['log_level'])  # 'INFO' (from config_file)
print(config['host'])       # '0.0.0.0' (from defaults)

Contextes à portée (scoped) avec new_child()

from collections import ChainMap
 
# Simulating variable scoping (like nested function scopes)
global_scope = {'x': 1, 'y': 2}
local_scope = ChainMap(global_scope)
 
# Enter a new scope
inner_scope = local_scope.new_child()
inner_scope['x'] = 10  # Shadows global x
inner_scope['z'] = 30  # New local variable
 
print(inner_scope['x'])  # 10 (local)
print(inner_scope['y'])  # 2  (falls through to global)
print(inner_scope['z'])  # 30 (local)
 
# Exit scope -- original is unchanged
print(local_scope['x'])  # 1 (global still intact)

Comparaison de tous les types de collections

TypeClasse de baseMutableCas d’usageAvantage clé
CounterdictOuiCompter des élémentsmost_common(), arithmétique multiset
defaultdictdictOuiAuto-initialiser les clés manquantesPas de KeyError, factory functions
deque--OuiFile double extrémitéO(1) aux deux extrémités, maxlen
namedtupletupleNonEnregistrements de données structurésAccès par champ, léger
OrderedDictdictOuiDicts sensibles à l’ordremove_to_end(), égalité avec ordre
ChainMap--OuiRecherches en couchesSuperposition de config, portées

Benchmarks de performance

Counter vs comptage manuel

from collections import Counter, defaultdict
import time
 
data = list(range(1000)) * 1000  # 1 million items, 1000 unique
 
# Method 1: Counter
start = time.perf_counter()
c = Counter(data)
counter_time = time.perf_counter() - start
 
# Method 2: defaultdict(int)
start = time.perf_counter()
dd = defaultdict(int)
for item in data:
    dd[item] += 1
dd_time = time.perf_counter() - start
 
# Method 3: Manual dict
start = time.perf_counter()
manual = {}
for item in data:
    manual[item] = manual.get(item, 0) + 1
manual_time = time.perf_counter() - start
 
print(f"Counter:         {counter_time:.4f}s")
print(f"defaultdict(int):{dd_time:.4f}s")
print(f"dict.get():      {manual_time:.4f}s")
# Typical: Counter ~0.03s, defaultdict ~0.07s, dict.get() ~0.09s

deque vs list pour des opérations de file

from collections import deque
import time
 
n = 100_000
 
# Simulate a FIFO queue: append right, pop left
# List
start = time.perf_counter()
q = list(range(n))
while q:
    q.pop(0)
list_queue_time = time.perf_counter() - start
 
# Deque
start = time.perf_counter()
q = deque(range(n))
while q:
    q.popleft()
deque_queue_time = time.perf_counter() - start
 
print(f"List pop(0):     {list_queue_time:.4f}s")
print(f"Deque popleft(): {deque_queue_time:.4f}s")
print(f"Deque is {list_queue_time / deque_queue_time:.0f}x faster")
# Typical: List ~2.5s, Deque ~0.004s -> ~600x faster

Exemples du monde réel

Analyse de logs avec Counter

from collections import Counter
from datetime import datetime
 
# Parse and analyze server logs
log_lines = [
    "2026-02-18 10:15:03 GET /api/users 200",
    "2026-02-18 10:15:04 POST /api/login 401",
    "2026-02-18 10:15:05 GET /api/users 200",
    "2026-02-18 10:15:06 GET /api/products 500",
    "2026-02-18 10:15:07 POST /api/login 200",
    "2026-02-18 10:15:08 GET /api/users 200",
    "2026-02-18 10:15:09 GET /api/products 500",
    "2026-02-18 10:15:10 POST /api/login 401",
]
 
# Count status codes
status_codes = Counter(line.split()[-1] for line in log_lines)
print("Status codes:", status_codes.most_common())
# [('200', 4), ('401', 2), ('500', 2)]
 
# Count endpoints
endpoints = Counter(line.split()[3] for line in log_lines)
print("Top endpoints:", endpoints.most_common(2))
# [('/api/users', 3), ('/api/login', 3)]
 
# Count error endpoints (status >= 400)
errors = Counter(
    line.split()[3] for line in log_lines
    if int(line.split()[-1]) >= 400
)
print("Error endpoints:", errors)
# Counter({'/api/login': 2, '/api/products': 2})

Gestion de configuration avec ChainMap

from collections import ChainMap
import json
 
# Multi-layer config system for a web application
def load_config(config_path=None, cli_overrides=None):
    # Layer 1: Hard-coded defaults
    defaults = {
        'host': '127.0.0.1',
        'port': 8000,
        'debug': False,
        'db_pool_size': 5,
        'log_level': 'WARNING',
        'cors_origins': ['http://localhost:3000'],
    }
 
    # Layer 2: Config file
    file_config = {}
    if config_path:
        with open(config_path) as f:
            file_config = json.load(f)
 
    # Layer 3: CLI overrides (highest priority)
    cli = cli_overrides or {}
 
    # ChainMap searches cli -> file_config -> defaults
    return ChainMap(cli, file_config, defaults)
 
# Usage
config = load_config(cli_overrides={'debug': True, 'port': 9000})
print(config['debug'])        # True (CLI override)
print(config['port'])         # 9000 (CLI override)
print(config['db_pool_size']) # 5    (default)
print(config['log_level'])    # WARNING (default)

Cache des éléments récents avec deque

from collections import deque
 
class RecentItemsTracker:
    """Track the N most recent unique items."""
 
    def __init__(self, max_items=10):
        self.items = deque(maxlen=max_items)
        self.seen = set()
 
    def add(self, item):
        if item in self.seen:
            # Move to front by removing and re-adding
            self.items.remove(item)
            self.items.append(item)
        else:
            if len(self.items) == self.items.maxlen:
                # Remove the oldest item from the set too
                oldest = self.items[0]
                self.seen.discard(oldest)
            self.items.append(item)
            self.seen.add(item)
 
    def get_recent(self):
        return list(reversed(self.items))
 
# Track recently viewed products
tracker = RecentItemsTracker(max_items=5)
for product in ['shoes', 'shirt', 'hat', 'shoes', 'jacket', 'belt', 'hat']:
    tracker.add(product)
 
print(tracker.get_recent())
# ['hat', 'belt', 'jacket', 'shoes', 'shirt']

Pipeline de données avec namedtuple

from collections import namedtuple, Counter, defaultdict
 
# Define structured records
Transaction = namedtuple('Transaction', 'id customer product amount date')
 
transactions = [
    Transaction(1, 'Alice', 'Widget', 29.99, '2026-02-01'),
    Transaction(2, 'Bob', 'Gadget', 49.99, '2026-02-01'),
    Transaction(3, 'Alice', 'Widget', 29.99, '2026-02-03'),
    Transaction(4, 'Charlie', 'Gadget', 49.99, '2026-02-05'),
    Transaction(5, 'Alice', 'Gizmo', 19.99, '2026-02-07'),
    Transaction(6, 'Bob', 'Widget', 29.99, '2026-02-08'),
]
 
# Most popular products
product_count = Counter(t.product for t in transactions)
print("Popular products:", product_count.most_common())
# [('Widget', 3), ('Gadget', 2), ('Gizmo', 1)]
 
# Revenue by customer
revenue = defaultdict(float)
for t in transactions:
    revenue[t.customer] += t.amount
print("Revenue:", dict(revenue))
# {'Alice': 79.97, 'Bob': 79.98, 'Charlie': 49.99}
 
# Convert to DataFrame for visualization
import pandas as pd
df = pd.DataFrame(transactions, columns=Transaction._fields)
print(df.groupby('customer')['amount'].sum())

Visualiser des données de collections avec PyGWalker

Après avoir traité des données avec Counter, defaultdict ou namedtuple, vous voulez souvent visualiser les résultats. PyGWalker (opens in a new tab) transforme n’importe quel pandas DataFrame en interface de visualisation interactive de type Tableau directement dans les notebooks Jupyter :

from collections import Counter
import pandas as pd
import pygwalker as pyg
 
# Process data with collections
log_data = ["ERROR", "WARNING", "ERROR", "INFO", "ERROR", "WARNING", "INFO", "INFO"]
counts = Counter(log_data)
 
# Convert to DataFrame
df = pd.DataFrame(counts.items(), columns=['Level', 'Count'])
 
# Launch interactive visualization
walker = pyg.walk(df)

Cela vous permet de glisser-déposer des champs, créer des graphiques, filtrer des données et explorer des patterns de manière interactive — sans écrire de code de visualisation. C’est particulièrement utile avec de grands jeux de données traités via des regroupements Counter ou defaultdict, quand vous voulez explorer visuellement les distributions.

Pour exécuter ces expériences sur les collections de manière interactive, RunCell (opens in a new tab) fournit un environnement Jupyter alimenté par l’IA où vous pouvez itérer sur des pipelines de traitement de données avec un feedback instantané.

Combiner plusieurs types de collections

La vraie puissance de collections apparaît quand vous combinez plusieurs types dans un même pipeline.

from collections import Counter, defaultdict, namedtuple, deque
 
# Named record type
LogEntry = namedtuple('LogEntry', 'timestamp level message')
 
# Simulated log stream
log_stream = deque([
    LogEntry('10:01', 'ERROR', 'Connection timeout'),
    LogEntry('10:02', 'INFO', 'Request processed'),
    LogEntry('10:03', 'ERROR', 'Connection timeout'),
    LogEntry('10:04', 'WARNING', 'High memory'),
    LogEntry('10:05', 'ERROR', 'Disk full'),
    LogEntry('10:06', 'INFO', 'Request processed'),
    LogEntry('10:07', 'ERROR', 'Connection timeout'),
], maxlen=100)
 
# Count error types
error_counts = Counter(
    entry.message for entry in log_stream if entry.level == 'ERROR'
)
print("Error types:", error_counts.most_common())
# [('Connection timeout', 3), ('Disk full', 1)]
 
# Group entries by level
by_level = defaultdict(list)
for entry in log_stream:
    by_level[entry.level].append(entry)
 
for level, entries in by_level.items():
    print(f"{level}: {len(entries)} entries")
# ERROR: 4 entries
# INFO: 2 entries
# WARNING: 1 entries

FAQ

Qu’est-ce que le module collections de Python ?

Le module collections fait partie de la bibliothèque standard de Python. Il fournit des types de conteneurs spécialisés qui étendent les types intégrés (dict, list, tuple, set) avec des fonctionnalités supplémentaires. Les principales classes sont Counter, defaultdict, deque, namedtuple, OrderedDict et ChainMap. Chacune résout plus efficacement qu’un type intégré une catégorie précise de problèmes de manipulation de données.

Quand utiliser Counter plutôt que defaultdict(int) ?

Utilisez Counter lorsque votre objectif principal est de compter des éléments ou de comparer des distributions de fréquences. Il fournit most_common(), des opérateurs arithmétiques (+, -, &, |) et peut compter un itérable entier via un seul appel au constructeur. Utilisez defaultdict(int) lorsque le comptage est secondaire dans un pattern de structure de données plus large, ou lorsque vous avez besoin d’un dictionnaire généraliste avec des valeurs entières par défaut.

deque est-il thread-safe en Python ?

Oui. Dans CPython, deque.append(), deque.appendleft(), deque.pop() et deque.popleft() sont des opérations atomiques grâce à la GIL (Global Interpreter Lock). Cela rend deque utilisable comme file thread-safe sans verrou supplémentaire. En revanche, les opérations composées (par exemple les séquences « tester puis agir ») nécessitent tout de même une synchronisation explicite.

Quelle est la différence entre namedtuple et dataclass ?

namedtuple crée des sous-classes de tuple immuables avec des champs nommés. C’est léger, compatible avec l’itération et le dépaquetage, et utilise très peu de mémoire. dataclass (module dataclasses, Python 3.7+) crée de vraies classes avec des attributs mutables par défaut, et supporte méthodes, propriétés et héritage. Utilisez namedtuple pour des enregistrements simples et immuables. Utilisez dataclass si vous avez besoin de mutabilité, de comportements complexes, ou d’annotations de type poussées.

OrderedDict est-il encore utile en Python 3.7+ ?

Oui, dans deux cas spécifiques. D’abord, l’égalité de OrderedDict tient compte de l’ordre des éléments (OrderedDict(a=1, b=2) != OrderedDict(b=2, a=1)), contrairement aux dicts standards. Ensuite, OrderedDict fournit move_to_end() pour réordonner les éléments, pratique pour implémenter des caches LRU et des structures basées sur la priorité. Pour tous les autres cas, un dict standard suffit et est généralement plus performant.

En quoi ChainMap diffère-t-il de la fusion de dictionnaires ?

ChainMap crée une vue sur plusieurs dictionnaires sans copier les données. Les recherches parcourent chaque dictionnaire dans l’ordre. Les modifications des dictionnaires sous-jacents sont immédiatement reflétées dans le ChainMap. À l’inverse, une fusion avec {**d1, **d2} ou d1 | d2 crée un nouveau dictionnaire en dupliquant toutes les données. ChainMap est plus économe en mémoire pour de gros dictionnaires et conserve la structure en couches, utile pour la configuration et les patterns de portée.

Puis-je utiliser les types de collections avec des type hints ?

Oui. Utilisez collections.Counter[str] pour des counters typés, collections.defaultdict[str, list[int]] pour des defaultdicts typés, et collections.deque[int] pour des deques typés. Pour namedtuple, préférez typing.NamedTuple, qui supporte directement les annotations de type dans la définition de la classe. Tous les types de collections sont compatibles avec mypy et d’autres type checkers.

Conclusion

Le module collections de Python propose six types de conteneurs spécialisés qui éliminent des patterns de code répétitifs. Counter remplace les boucles manuelles de comptage. defaultdict supprime la gestion des KeyError. deque fournit des opérations rapides en double extrémité. namedtuple ajoute des noms de champs lisibles aux tuples. OrderedDict gère les comparaisons sensibles à l’ordre et le réordonnancement. ChainMap gère les recherches en couches dans des dictionnaires sans duplication des données.

Chaque type répond à un problème précis mieux que les conteneurs intégrés. Savoir quand utiliser chacun rendra votre code Python plus court, plus rapide et plus facile à maintenir. L’essentiel est d’associer la structure de données au pattern d’opérations : comptage (Counter), regroupement (defaultdict), file/pile (deque), enregistrements structurés (namedtuple), opérations ordonnées (OrderedDict) et recherches en couches (ChainMap).

📚