# Fichier: python_cheats/cheatsheets/collections.txt
# Cheatsheet Module Collections Python - Guide Complet


[OK] INTRODUCTION

# Le module collections fournit des types de conteneurs spécialisés
# Alternative aux dict, list, set et tuple standards
import collections
from collections import *

# Conteneurs disponibles:
# - namedtuple()   : tuple avec champs nommés
# - deque          : liste double-ended (files, piles)
# - Counter        : compteur de hashables
# - OrderedDict    : dictionnaire qui garde l'ordre d'insertion
# - defaultdict    : dict avec valeur par défaut
# - ChainMap       : groupe plusieurs dicts en une vue
# - UserDict       : wrapper pour créer des sous-classes de dict
# - UserList       : wrapper pour créer des sous-classes de list
# - UserString     : wrapper pour créer des sous-classes de str


[OK] NAMEDTUPLE - Tuples avec Champs Nommés


# === CRÉATION ===

from collections import namedtuple

# Syntaxe basique
Point = namedtuple('Point', ['x', 'y'])
Point = namedtuple('Point', 'x y')          # Avec espaces
Point = namedtuple('Point', 'x,y')          # Avec virgules

# Créer instance
p = Point(11, 22)
p = Point(x=11, y=22)

# Accès aux valeurs
print(p.x)          # 11
print(p.y)          # 22
print(p[0])         # 11 (aussi accessible par index)
print(p[1])         # 22

# === EXEMPLES PRATIQUES ===

# Coordonnées
Point = namedtuple('Point', ['x', 'y'])
p1 = Point(10, 20)
p2 = Point(x=30, y=40)

# Personne
Person = namedtuple('Person', ['name', 'age', 'city'])
person = Person('Alice', 30, 'Paris')
print(person.name)      # Alice
print(person.age)       # 30

# RGB Couleur
Color = namedtuple('Color', ['red', 'green', 'blue'])
white = Color(255, 255, 255)
black = Color(0, 0, 0)
red = Color(red=255, green=0, blue=0)

# Employé
Employee = namedtuple('Employee', 'name position salary')
emp = Employee('Bob', 'Developer', 75000)

# === MÉTHODES ===

# Unpacking
Point = namedtuple('Point', 'x y')
p = Point(11, 22)
x, y = p
print(x, y)         # 11 22

# Convertir en dict
print(p._asdict())          # {'x': 11, 'y': 22}

# Créer depuis dict
d = {'x': 30, 'y': 40}
p2 = Point(**d)

# Créer depuis iterable
data = [50, 60]
p3 = Point(*data)

# Remplacer valeurs (retourne nouveau tuple)
Point = namedtuple('Point', 'x y')
p = Point(11, 22)
p2 = p._replace(x=33)
print(p2)           # Point(x=33, y=22)

# Valeurs par défaut (Python 3.7+)
Person = namedtuple('Person', ['name', 'age', 'city'], defaults=['Unknown', 0, 'Unknown'])
p1 = Person('Alice')                    # Person(name='Alice', age=0, city='Unknown')
p2 = Person('Bob', 25)                  # Person(name='Bob', age=25, city='Unknown')

# Obtenir les champs
print(Point._fields)        # ('x', 'y')

# Créer nouvelle namedtuple avec champs supplémentaires
Point3D = namedtuple('Point3D', Point._fields + ('z',))
p3d = Point3D(1, 2, 3)

# === COMPARAISON AVEC TUPLE NORMAL ===

# Tuple normal
person_tuple = ('Alice', 30, 'Paris')
print(person_tuple[0])      # Pas clair: que représente 0?

# Namedtuple
Person = namedtuple('Person', 'name age city')
person = Person('Alice', 30, 'Paris')
print(person.name)          # Clair et lisible!

# === CAS D'USAGE ===

# 1. Retour de fonction
def get_user():
    User = namedtuple('User', 'id name email')
    return User(1, 'Alice', 'alice@example.com')

user = get_user()
print(user.name)    # Alice

# 2. Coordonnées géographiques
Location = namedtuple('Location', 'latitude longitude')
paris = Location(48.8566, 2.3522)

# 3. Simulation de portée (scope)
global_vars = {'x': 10}
local_vars = {'x': 20, 'y': 30}
scope = ChainMap(local_vars, global_vars)
print(scope['x'])       # 20 (local shadowing)
print(scope['y'])       # 30 (local only)

# 4. Fusion temporaire sans modifier originaux
dict1 = {'a': 1}
dict2 = {'b': 2}
combined = ChainMap(dict1, dict2)
# dict1 et dict2 restent inchangés

# 5. CLI arguments + config + defaults
import argparse
parser = argparse.ArgumentParser()
args = vars(parser.parse_args())
config_file = {'timeout': 30}
defaults = {'host': 'localhost', 'timeout': 10}
final_config = ChainMap(args, config_file, defaults)


[OK] USERDICT, USERLIST, USERSTRING - Classes de Base


# Ces classes facilitent la création de sous-classes personnalisées

# === USERDICT ===

from collections import UserDict

class CaseInsensitiveDict(UserDict):
    """Dict insensible à la casse"""
    
    def __getitem__(self, key):
        return super().__getitem__(key.lower())
    
    def __setitem__(self, key, value):
        super().__setitem__(key.lower(), value)
    
    def __delitem__(self, key):
        super().__delitem__(key.lower())
    
    def __contains__(self, key):
        return super().__contains__(key.lower())

d = CaseInsensitiveDict({'Name': 'Alice'})
print(d['name'])        # 'Alice'
print(d['NAME'])        # 'Alice'
d['EMAIL'] = 'alice@example.com'
print('email' in d)     # True

# Compteur avec historique
class CounterWithHistory(UserDict):
    def __init__(self):
        super().__init__()
        self.history = []
    
    def __setitem__(self, key, value):
        self.history.append((key, value))
        super().__setitem__(key, value)

# Dict avec validation
class TypedDict(UserDict):
    def __init__(self, value_type):
        super().__init__()
        self.value_type = value_type
    
    def __setitem__(self, key, value):
        if not isinstance(value, self.value_type):
            raise TypeError(f"Value must be {self.value_type}")
        super().__setitem__(key, value)

int_dict = TypedDict(int)
int_dict['count'] = 10      # OK
# int_dict['name'] = 'Alice'  # TypeError

# === USERLIST ===

from collections import UserList

class UniqueList(UserList):
    """Liste qui n'accepte pas de doublons"""
    
    def append(self, item):
        if item not in self.data:
            super().append(item)
    
    def extend(self, items):
        for item in items:
            self.append(item)

ul = UniqueList([1, 2, 3])
ul.append(2)        # Ignoré
ul.append(4)
print(ul.data)      # [1, 2, 3, 4]

# Liste avec statistiques
class StatsList(UserList):
    @property
    def mean(self):
        return sum(self.data) / len(self.data) if self.data else 0
    
    @property
    def max(self):
        return max(self.data) if self.data else None

sl = StatsList([10, 20, 30])
print(sl.mean)      # 20.0

# === USERSTRING ===

from collections import UserString

class UpperString(UserString):
    """String toujours en majuscules"""
    
    def __init__(self, seq):
        super().__init__(seq.upper())
    
    def __add__(self, other):
        return UpperString(self.data + str(other).upper())

us = UpperString("hello")
print(us)           # HELLO
print(us + " world")  # HELLO WORLD

# String avec méthodes personnalisées
class SmartString(UserString):
    def words(self):
        return self.data.split()
    
    def reverse_words(self):
        return ' '.join(reversed(self.words()))

ss = SmartString("hello world python")
print(ss.reverse_words())   # python world hello


[OK] COMPARAISON DES CONTENEURS


# === QUAND UTILISER CHAQUE CONTENEUR ===

# namedtuple
# [OK] Données structurées immutables
# [OK] Alternative légère aux classes
# [OK] Tuples avec noms de champs
# [X] Immutable (utiliser dataclass si besoin mutabilité)

# deque
# [OK] Files (queues)
# [OK] Piles (stacks)
# [OK] Ajouts/retraits aux extrémités fréquents
# [OK] Fenêtres glissantes
# [X] Accès par index (lent)

# Counter
# [OK] Compter occurrences
# [OK] Statistiques simples
# [OK] Opérations sur multisets
# [X] Comptages négatifs peuvent surprendre

# defaultdict
# [OK] Grouper données
# [OK] Construire structures (graphes, index)
# [OK] Éviter vérifications d'existence de clés
# [X] Peut cacher erreurs (clés inexistantes retournent défaut)

# OrderedDict
# [OK] Compatibilité Python < 3.7
# [OK] move_to_end() nécessaire
# [OK] Comparaison sensible à l'ordre
# [X] dict normal suffit souvent (Python 3.7+)

# ChainMap
# [OK] Configurations hiérarchiques
# [OK] Contextes imbriqués
# [OK] Fusion temporaire de dicts
# [X] Modifications affectent premier dict seulement

# UserDict/List/String
# [OK] Créer sous-classes personnalisées
# [OK] Éviter pièges de sous-classer dict/list/str directement
# [X] Overhead léger vs types built-in


[OK] EXEMPLES PRATIQUES COMPLETS


# === 1. SYSTÈME DE CACHE LRU ===

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 None
        self.cache.move_to_end(key)
        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:
            oldest = next(iter(self.cache))
            del self.cache[oldest]

cache = LRUCache(3)
cache.put('a', 1)
cache.put('b', 2)
cache.put('c', 3)
cache.put('d', 4)   # 'a' est évincé
print(cache.get('b'))  # 2

# === 2. ANALYSEUR DE LOGS ===

from collections import Counter, defaultdict, namedtuple
from datetime import datetime

LogEntry = namedtuple('LogEntry', ['timestamp', 'level', 'message'])

class LogAnalyzer:
    def __init__(self):
        self.entries = []
        self.level_counts = Counter()
        self.errors_by_hour = defaultdict(list)
    
    def add_entry(self, timestamp, level, message):
        entry = LogEntry(timestamp, level, message)
        self.entries.append(entry)
        self.level_counts[level] += 1
        
        if level == 'ERROR':
            hour = timestamp.hour
            self.errors_by_hour[hour].append(message)
    
    def most_common_errors(self, n=5):
        error_messages = [e.message for e in self.entries if e.level == 'ERROR']
        return Counter(error_messages).most_common(n)
    
    def peak_error_hours(self):
        return sorted(self.errors_by_hour.items(), 
                     key=lambda x: len(x[1]), 
                     reverse=True)[:3]

# === 3. GESTIONNAIRE DE CONFIGURATION ===

from collections import ChainMap

class ConfigManager:
    def __init__(self):
        self.defaults = {
            'host': 'localhost',
            'port': 8000,
            'debug': False,
            'timeout': 30
        }
        self.env_config = {}
        self.user_config = {}
        self.runtime_config = {}
    
    def get_config(self):
        return ChainMap(
            self.runtime_config,
            self.user_config,
            self.env_config,
            self.defaults
        )
    
    def set_runtime(self, key, value):
        self.runtime_config[key] = value
    
    def load_user_config(self, config_dict):
        self.user_config.update(config_dict)

config_manager = ConfigManager()
config_manager.load_user_config({'port': 9000, 'debug': True})
config_manager.set_runtime('timeout', 60)
config = config_manager.get_config()
print(config['port'])       # 9000
print(config['timeout'])    # 60

# === 4. SYSTÈME DE TÂCHES (ROUND-ROBIN) ===

from collections import deque

class TaskScheduler:
    def __init__(self):
        self.tasks = deque()
        self.completed = []
    
    def add_task(self, task):
        self.tasks.append(task)
    
    def execute_next(self):
        if not self.tasks:
            return None
        
        task = self.tasks.popleft()
        result = task.execute()
        
        if not task.is_complete():
            self.tasks.append(task)  # Retour à la fin
        else:
            self.completed.append(task)
        
        return result
    
    def run_all(self):
        while self.tasks:
            self.execute_next()

# === 5. ANALYSEUR DE TEXTE ===

from collections import Counter, defaultdict

class TextAnalyzer:
    def __init__(self, text):
        self.text = text.lower()
        self.words = self.text.split()
        self.word_freq = Counter(self.words)
        self.char_freq = Counter(self.text.replace(' ', ''))
    
    def most_common_words(self, n=10):
        return self.word_freq.most_common(n)
    
    def most_common_chars(self, n=10):
        return self.char_freq.most_common(n)
    
    def word_length_distribution(self):
        lengths = defaultdict(int)
        for word in self.words:
            lengths[len(word)] += 1
        return dict(lengths)
    
    def find_anagrams(self):
        anagram_groups = defaultdict(list)
        for word in set(self.words):
            key = ''.join(sorted(word))
            anagram_groups[key].append(word)
        return {k: v for k, v in anagram_groups.items() if len(v) > 1}

text = "hello world hello python world python programming"
analyzer = TextAnalyzer(text)
print(analyzer.most_common_words(3))
print(analyzer.word_length_distribution())

# === 6. GRAPHE (ADJACENCY LIST) ===

from collections import defaultdict, deque

class Graph:
    def __init__(self):
        self.graph = defaultdict(list)
    
    def add_edge(self, u, v):
        self.graph[u].append(v)
    
    def bfs(self, start):
        visited = set()
        queue = deque([start])
        visited.add(start)
        
        while queue:
            vertex = queue.popleft()
            print(vertex, end=' ')
            
            for neighbor in self.graph[vertex]:
                if neighbor not in visited:
                    visited.add(neighbor)
                    queue.append(neighbor)

g = Graph()
g.add_edge('A', 'B')
g.add_edge('A', 'C')
g.add_edge('B', 'D')
g.add_edge('C', 'D')
g.bfs('A')  # A B C D

# === 7. SYSTÈME DE VOTES ===

from collections import Counter, namedtuple

Vote = namedtuple('Vote', ['voter', 'candidate', 'timestamp'])

class VotingSystem:
    def __init__(self):
        self.votes = []
        self.voters = set()
    
    def cast_vote(self, vote):
        if vote.voter in self.voters:
            return False  # Déjà voté
        self.votes.append(vote)
        self.voters.add(vote.voter)
        return True
    
    def get_results(self):
        candidates = [v.candidate for v in self.votes]
        return Counter(candidates)
    
    def get_winner(self):
        results = self.get_results()
        if not results:
            return None
        return results.most_common(1)[0]

voting = VotingSystem()
voting.cast_vote(Vote('Alice', 'Candidate A', '2024-01-01'))
voting.cast_vote(Vote('Bob', 'Candidate B', '2024-01-01'))
voting.cast_vote(Vote('Charlie', 'Candidate A', '2024-01-01'))
winner, votes = voting.get_winner()
print(f"Gagnant: {winner} avec {votes} votes")

# === 8. UNDO/REDO SYSTEM ===

from collections import deque

class UndoRedoManager:
    def __init__(self, max_history=100):
        self.undo_stack = deque(maxlen=max_history)
        self.redo_stack = deque(maxlen=max_history)
    
    def do_action(self, action):
        action.execute()
        self.undo_stack.append(action)
        self.redo_stack.clear()
    
    def undo(self):
        if not self.undo_stack:
            return False
        action = self.undo_stack.pop()
        action.undo()
        self.redo_stack.append(action)
        return True
    
    def redo(self):
        if not self.redo_stack:
            return False
        action = self.redo_stack.pop()
        action.execute()
        self.undo_stack.append(action)
        return True


[OK] PERFORMANCE & COMPLEXITÉ


# === COMPLEXITÉ TEMPORELLE ===

# namedtuple
# - Création: O(1)
# - Accès: O(1)
# - Immutable, donc pas de modification

# deque
# - append()/appendleft(): O(1)
# - pop()/popleft(): O(1)
# - Accès index: O(n)
# - insert(i, x): O(n)
# - rotate(): O(k)

# Counter
# - Construction: O(n)
# - Accès: O(1)
# - most_common(k): O(n log k)
# - Opérations (+, -, &, |): O(n + m)

# defaultdict
# - Même que dict
# - Accès: O(1) moyenne
# - Insertion: O(1) moyenne

# OrderedDict
# - Accès/Insertion/Suppression: O(1)
# - move_to_end(): O(1)
# - Python 3.7+: dict normal a même performance

# ChainMap
# - Accès: O(n) où n = nombre de dicts
# - Insertion/Suppression: O(1) (premier dict)

# === UTILISATION MÉMOIRE ===

import sys
from collections import *

# Comparaison mémoire
regular_dict = {i: i for i in range(1000)}
default_dict = defaultdict(int, regular_dict)
ordered_dict = OrderedDict(regular_dict)

print(f"dict: {sys.getsizeof(regular_dict)} bytes")
print(f"defaultdict: {sys.getsizeof(default_dict)} bytes")
print(f"OrderedDict: {sys.getsizeof(ordered_dict)} bytes")

# namedtuple vs dict
Point = namedtuple('Point', ['x', 'y'])
point_tuple = Point(10, 20)
point_dict = {'x': 10, 'y': 20}

print(f"namedtuple: {sys.getsizeof(point_tuple)} bytes")
print(f"dict: {sys.getsizeof(point_dict)} bytes")
# namedtuple est plus léger!

# deque vs list (pour opérations spécifiques)
from collections import deque
import timeit

# Ajout au début
list_time = timeit.timeit('lst.insert(0, 1)', 'lst = list(range(1000))', number=10000)
deque_time = timeit.timeit('d.appendleft(1)', 'd = __import__("collections").deque(range(1000))', number=10000)
print(f"Insert au début - list: {list_time:.4f}s, deque: {deque_time:.4f}s")


[OK] BONNES PRATIQUES


# === 1. Choisir le bon conteneur ===

# Mauvais: utiliser list pour file
queue = []
queue.append(1)
queue.pop(0)        # O(n) - lent!

# Bon: utiliser deque
from collections import deque
queue = deque()
queue.append(1)
queue.popleft()     # O(1) - rapide!

# === 2. Initialisation propre de defaultdict ===

# Mauvais: lambda complexe
d = defaultdict(lambda: {'count': 0, 'items': []})

# Bon: fonction nommée
def default_value():
    return {'count': 0, 'items': []}

d = defaultdict(default_value)

# === 3. Counter pour comptages ===

# Mauvais: dict manuel
counts = {}
for item in items:
    if item in counts:
        counts[item] += 1
    else:
        counts[item] = 1

# Bon: Counter
from collections import Counter
counts = Counter(items)

# === 4. namedtuple pour structures de données ===

# Mauvais: indices magiques
person = ('Alice', 30, 'Paris')
print(person[1])    # Quel champ?

# Bon: namedtuple
from collections import namedtuple
Person = namedtuple('Person', ['name', 'age', 'city'])
person = Person('Alice', 30, 'Paris')
print(person.age)   # Clair!

# === 5. Éviter modifications pendant itération ===

# Mauvais
d = defaultdict(list)
for key in d:
    d[key].append(1)    # Peut causer problèmes

# Bon
d = defaultdict(list)
for key in list(d.keys()):
    d[key].append(1)

# === 6. Vérifier existence avec Counter ===

# Counter retourne 0 pour clés inexistantes
c = Counter({'a': 5})
print(c['b'])       # 0, pas KeyError

# Utiliser .get() si besoin distinguer absence vs 0
if c.get('b') is None:
    print("Clé absente")

# === 7. ChainMap pour configuration ===

# Bon pattern pour configs hiérarchiques
from collections import ChainMap
config = ChainMap(cli_args, env_vars, config_file, defaults)

# === 8. Type hints ===

from typing import Counter as CounterType, DefaultDict, Deque
from collections import Counter, defaultdict, deque

def count_words(text: str) -> CounterType[str]:
    return Counter(text.split())

def group_by_key(items: list) -> DefaultDict[str, list]:
    groups = defaultdict(list)
    # ...
    return groups


[OK] PIÈGES COURANTS


# === 1. defaultdict avec lambda ===

# Piège: lambda appelé à chaque accès
d = defaultdict(lambda: [])
d['a'].append(1)
d['a'].append(2)    # OK, même liste

# Attention: nouvelles clés créent nouvelles listes
d['b']              # Crée liste vide
d['b'] is d['b']    # True (même objet)

# === 2. Counter avec éléments non-hashables ===

# Erreur
# c = Counter([[1, 2], [3, 4]])  # TypeError

# Solution: convertir en tuple
c = Counter([tuple(x) for x in [[1, 2], [3, 4]]])

# === 3. OrderedDict popitem() ===

from collections import OrderedDict

od = OrderedDict([('a', 1), ('b', 2)])
# Python 3.7+: dict aussi garde l'ordre
# Mais dict.popitem() ne garantit pas LIFO

# === 4. ChainMap modifications ===

from collections import ChainMap

d1 = {'a': 1}
d2 = {'b': 2}
cm = ChainMap(d1, d2)

# Modifications affectent premier dict seulement!
cm['c'] = 3         # d1['c'] = 3, pas d2
del cm['a']         # del d1['a']
# del cm['b']       # KeyError! 'b' est dans d2

# === 5. deque avec maxlen ===

from collections import deque

d = deque([1, 2, 3], maxlen=3)
d.append(4)         # [2, 3, 4] - 1 automatiquement retiré!

# Attention avec extend
d = deque([1], maxlen=3)
d.extend([2, 3, 4, 5])  # [3, 4, 5] - seuls 3 derniers gardés!

# === 6. namedtuple immutabilité ===

Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
# p.x = 10          # AttributeError!

# Utiliser _replace()
p = p._replace(x=10)

# === 7. Counter avec valeurs négatives ===

c1 = Counter(a=4, b=2)
c2 = Counter(a=1, b=3)
c3 = c1 - c2        # Counter({'a': 3})
# 'b' disparaît car 2-3 = -1 (négatif)

# Pour garder négatifs:
c1.subtract(c2)     # Counter({'a': 3, 'b': -1})

# === 8. Type confusion ===

# Counter hérite de dict
c = Counter()
isinstance(c, dict)     # True

# defaultdict aussi
d = defaultdict(int)
isinstance(d, dict)     # True

# Mais comportement différent!


[OK] RESSOURCES & DOCUMENTATION


# Documentation officielle:
# https://docs.python.org/3/library/collections.html

# PEPs pertinents:
# PEP 584 - Union Operators for dicts (Python 3.9+)
# PEP 3132 - Extended Iterable Unpacking

# Articles recommandés:
# - Real Python: Collections Module
# - Python.org: collections documentation
# - PyMOTW-3: collections examples

# Modules associés:
import collections.abc     # Abstract Base Classes
import heapq              # Priority queues
import itertools          # Itertools (complément à collections)
import functools          # lru_cache (cache avancé)

# Alternatives modernes:
# - dataclasses (Python 3.7+) : alternative à namedtuple
# - typing (type hints pour collections)
# - attrs/pydantic : validation de données


[OK] ASTUCES & PATTERNS AVANCÉS


# === Pattern: Dict de Counters ===

from collections import defaultdict, Counter

# Compter par catégorie
by_category = defaultdict(Counter)
for item in items:
    by_category[item.category][item.name] += 1

# === Pattern: Nested defaultdict ===

def nested_dict():
    return defaultdict(nested_dict)

tree = nested_dict()
tree['a']['b']['c'] = 1
tree['a']['b']['d'] = 2

# === Pattern: Counter comme multiset ===

from collections import Counter

# Union, intersection, etc.
bag1 = Counter(['a', 'b', 'c', 'a'])
bag2 = Counter(['a', 'c', 'd'])

# Tous éléments
bag1 | bag2         # Counter({'a': 2, 'c': 1, 'b': 1, 'd': 1})

# Éléments communs
bag1 & bag2         # Counter({'a': 1, 'c': 1})

# === Pattern: deque comme buffer circulaire ===

from collections import deque

class CircularBuffer:
    def __init__(self, size):
        self.buffer = deque(maxlen=size)
    
    def append(self, item):
        self.buffer.append(item)
    
    def get_all(self):
        return list(self.buffer)

# === Pattern: LRU avec OrderedDict ===

from collections import OrderedDict
from functools import wraps

def lru_cache_manual(maxsize=128):
    cache = OrderedDict()
    
    def decorator(func):
        @wraps(func)
        def wrapper(*args):
            if args in cache:
                cache.move_to_end(args)
                return cache[args]
            
            result = func(*args)
            cache[args] = result
            
            if len(cache) > maxsize:
                cache.popitem(last=False)
            
            return result
        return wrapper
    return decorator

# === Pattern: Compteur avec seuil ===

class ThresholdCounter(Counter):
    def __init__(self, threshold, *args, **kwargs):
        self.threshold = threshold
        super().__init__(*args, **kwargs)
    
    def above_threshold(self):
        return {k: v for k, v in self.items() if v >= self.threshold}

# === Pattern: ChainMap pour scopes ===

class ScopeManager:
    def __init__(self):
        self.scopes = [{}]  # Global scope
    
    def push_scope(self):
        self.scopes.append({})
    
    def pop_scope(self):
        if len(self.scopes) > 1:
            self.scopes.pop()
    
    def set_var(self, name, value):
        self.scopes[-1][name] = value
    
    def get_var(self, name):
        chain = ChainMap(*reversed(self.scopes))
        return chain[name] Configuration
Config = namedtuple('Config', 'host port debug')
config = Config('localhost', 8000, True)

# 4. Données CSV
from csv import reader
Employee = namedtuple('Employee', 'name department salary')
with open('employees.csv') as f:
    for row in reader(f):
        emp = Employee(*row)
        print(emp.name, emp.salary)

# === HÉRITAGE ===

# Créer classe basée sur namedtuple
Point = namedtuple('Point', 'x y')

class Point3D(Point):
    __slots__ = ()  # Économie mémoire
    
    def __new__(cls, x, y, z):
        return super().__new__(cls, x, y)
    
    def __init__(self, x, y, z):
        self.z = z
    
    def distance_from_origin(self):
        return (self.x**2 + self.y**2 + self.z**2) ** 0.5


[OK] DEQUE - Double-Ended Queue


# === CRÉATION ===

from collections import deque

# Créer deque vide
d = deque()

# Créer depuis iterable
d = deque([1, 2, 3, 4, 5])
d = deque('abcde')
d = deque(range(5))

# Limiter taille (FIFO automatique quand pleine)
d = deque(maxlen=3)
d.append(1)     # deque([1])
d.append(2)     # deque([1, 2])
d.append(3)     # deque([1, 2, 3])
d.append(4)     # deque([2, 3, 4]) - 1 automatiquement retiré

# === OPÉRATIONS DE BASE ===

d = deque([1, 2, 3])

# Ajouter à droite
d.append(4)             # deque([1, 2, 3, 4])

# Ajouter à gauche
d.appendleft(0)         # deque([0, 1, 2, 3, 4])

# Retirer à droite
x = d.pop()             # x=4, deque([0, 1, 2, 3])

# Retirer à gauche
x = d.popleft()         # x=0, deque([1, 2, 3])

# Étendre à droite
d.extend([4, 5])        # deque([1, 2, 3, 4, 5])

# Étendre à gauche
d.extendleft([0, -1])   # deque([-1, 0, 1, 2, 3, 4, 5])
# Note: extendleft inverse l'ordre!

# === ROTATION ===

d = deque([1, 2, 3, 4, 5])

# Rotation à droite
d.rotate(2)             # deque([4, 5, 1, 2, 3])

# Rotation à gauche
d.rotate(-2)            # deque([1, 2, 3, 4, 5])

# === AUTRES MÉTHODES ===

d = deque([1, 2, 3, 2, 4])

# Compter occurrences
print(d.count(2))       # 2

# Retirer première occurrence
d.remove(2)             # deque([1, 3, 2, 4])

# Inverser
d.reverse()             # deque([4, 2, 3, 1])

# Vider
d.clear()               # deque([])

# Copier
d1 = deque([1, 2, 3])
d2 = d1.copy()          # Python 3.5+

# Index
d = deque([1, 2, 3, 2, 4])
print(d.index(2))       # 1
print(d.index(2, 2))    # 3 (chercher à partir de l'index 2)

# Insérer
d = deque([1, 2, 4])
d.insert(2, 3)          # deque([1, 2, 3, 4])

# Accès par index
d = deque([1, 2, 3, 4])
print(d[0])             # 1
print(d[-1])            # 4
d[1] = 20               # deque([1, 20, 3, 4])

# === CAS D'USAGE ===

# 1. File (FIFO - First In First Out)
queue = deque()
queue.append('Alice')       # Enfile
queue.append('Bob')
queue.append('Charlie')
print(queue.popleft())      # 'Alice' - Défile

# 2. Pile (LIFO - Last In First Out)
stack = deque()
stack.append(1)             # Push
stack.append(2)
stack.append(3)
print(stack.pop())          # 3 - Pop

# 3. Fenêtre glissante
def moving_average(data, window_size):
    window = deque(maxlen=window_size)
    for value in data:
        window.append(value)
        if len(window) == window_size:
            yield sum(window) / window_size

data = [10, 20, 30, 40, 50]
for avg in moving_average(data, 3):
    print(avg)

# 4. Historique limité
history = deque(maxlen=5)
history.append('command1')
history.append('command2')
# ... seules les 5 dernières commandes sont gardées

# 5. Rotation de tâches (Round Robin)
tasks = deque(['task1', 'task2', 'task3'])
while tasks:
    task = tasks.popleft()
    print(f"Exécution: {task}")
    if should_repeat(task):
        tasks.append(task)  # Remettre à la fin

# 6. Palindrome checker
def is_palindrome(word):
    d = deque(word.lower())
    while len(d) > 1:
        if d.popleft() != d.pop():
            return False
    return True

# === PERFORMANCE ===

# deque vs list pour opérations aux extrémités:
# deque.append()       O(1)     list.append()      O(1)
# deque.appendleft()   O(1)     list.insert(0)     O(n)
# deque.pop()          O(1)     list.pop()         O(1)
# deque.popleft()      O(1)     list.pop(0)        O(n)
# deque[i]             O(n)     list[i]            O(1)

# Utilisez deque pour:
# - Files (queues)
# - Piles (stacks) si vous voulez aussi appendleft
# - Fenêtres glissantes
# - Rotations

# Utilisez list pour:
# - Accès par index fréquent
# - Modifications au milieu


[OK] COUNTER - Compteur de Hashables


# === CRÉATION ===

from collections import Counter

# Créer vide
c = Counter()

# Depuis iterable
c = Counter([1, 2, 2, 3, 3, 3])     # Counter({3: 3, 2: 2, 1: 1})
c = Counter('abracadabra')          # Counter({'a': 5, 'b': 2, 'r': 2, ...})
c = Counter('hello world'.split())  # Counter des mots

# Depuis dict
c = Counter({'red': 4, 'blue': 2})

# Depuis kwargs
c = Counter(cats=4, dogs=8)

# === ACCÈS ===

c = Counter('abracadabra')

# Accès comme dict
print(c['a'])           # 5
print(c['z'])           # 0 (pas d'erreur, retourne 0!)

# Get
print(c.get('a'))       # 5
print(c.get('z', 0))    # 0

# Clés
print(list(c.keys()))   # ['a', 'b', 'r', 'c', 'd']

# Valeurs
print(list(c.values())) # [5, 2, 2, 1, 1]

# Items
print(list(c.items()))  # [('a', 5), ('b', 2), ...]

# === MÉTHODES SPÉCIALES ===

c = Counter(a=4, b=2, c=0, d=-2)

# most_common([n]) - n éléments les plus communs
print(c.most_common(2))     # [('a', 4), ('b', 2)]
print(c.most_common())      # Tous, triés par compte

# elements() - itérateur répétant chaque élément selon son compte
print(list(Counter('abc').elements()))  # ['a', 'b', 'c']
print(list(Counter(a=4, b=2).elements()))  # ['a', 'a', 'a', 'a', 'b', 'b']

# subtract() - soustraire compteurs
c1 = Counter(a=4, b=2, c=0)
c2 = Counter(a=1, b=2, d=3)
c1.subtract(c2)
print(c1)               # Counter({'a': 3, 'c': 0, 'b': 0, 'd': -3})

# update() - ajouter compteurs
c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2, c=5)
c1.update(c2)
print(c1)               # Counter({'c': 5, 'a': 4, 'b': 3})

# total() - somme de tous les comptes (Python 3.10+)
c = Counter(a=10, b=5, c=0)
print(c.total())        # 15

# === OPÉRATIONS MATHÉMATIQUES ===

c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2)

# Addition
c3 = c1 + c2            # Counter({'a': 4, 'b': 3})

# Soustraction (garde seulement positifs)
c3 = c1 - c2            # Counter({'a': 2})

# Intersection (minimum)
c3 = c1 & c2            # Counter({'a': 1, 'b': 1})

# Union (maximum)
c3 = c1 | c2            # Counter({'a': 3, 'b': 2})

# Unary plus (garde seulement positifs)
c = Counter(a=2, b=-3, c=0)
+c                      # Counter({'a': 2})

# Unary minus (garde seulement négatifs, inverse signe)
-c                      # Counter({'b': 3})

# === CAS D'USAGE ===

# 1. Compter fréquences de mots
text = "hello world hello python world"
word_counts = Counter(text.split())
print(word_counts.most_common(2))   # [('hello', 2), ('world', 2)]

# 2. Compter caractères
char_counts = Counter("hello")
print(char_counts)      # Counter({'l': 2, 'h': 1, 'e': 1, 'o': 1})

# 3. Analyser votes
votes = ['Alice', 'Bob', 'Alice', 'Charlie', 'Alice', 'Bob']
vote_counts = Counter(votes)
winner = vote_counts.most_common(1)[0][0]
print(f"Gagnant: {winner}")

# 4. Anagrammes
def are_anagrams(word1, word2):
    return Counter(word1) == Counter(word2)

print(are_anagrams('listen', 'silent'))  # True

# 5. Trouver éléments manquants
all_items = Counter(range(1, 11))  # 1 à 10
present = Counter([1, 2, 2, 3, 4, 5, 6, 7, 8, 9])
missing = all_items - present
print(list(missing.elements()))  # [10]

# 6. Top N éléments
data = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
top_3 = Counter(data).most_common(3)

# 7. Distribution de probabilité
from random import choices
outcomes = Counter(choices(['heads', 'tails'], k=1000))
print(f"Heads: {outcomes['heads']/1000:.2%}")

# 8. Multiset operations
bag1 = Counter(['a', 'b', 'c', 'a'])
bag2 = Counter(['a', 'c', 'd'])
intersection = bag1 & bag2      # Éléments communs
union = bag1 | bag2             # Tous éléments
difference = bag1 - bag2        # Dans bag1 mais pas bag2

# === CONVERSION ===

c = Counter(a=4, b=2, c=0)

# Vers dict
d = dict(c)

# Vers list de tuples
lst = list(c.items())

# Vers list d'éléments
elements = list(c.elements())

# Depuis dict
d = {'a': 4, 'b': 2}
c = Counter(d)

# === PATTERNS AVANCÉS ===

# Compter top N avec limite mémoire
from heapq import nlargest
def top_n_items(iterable, n):
    counts = Counter(iterable)
    return nlargest(n, counts.items(), key=lambda x: x[1])

# Filtrer par seuil
c = Counter(a=10, b=5, c=2, d=1)
filtered = Counter({k: v for k, v in c.items() if v >= 3})

# Normaliser (probabilités)
total = sum(c.values())
probs = {k: v/total for k, v in c.items()}

# Statistiques basiques
counts = Counter([1, 2, 2, 3, 3, 3, 4, 4, 4, 4])
mode = counts.most_common(1)[0][0]          # Mode
mean = sum(k*v for k,v in counts.items()) / sum(counts.values())


[OK] DEFAULTDICT - Dict avec Valeur par Défaut


# === CRÉATION ===

from collections import defaultdict

# Avec int (défaut: 0)
d = defaultdict(int)
d['count'] += 1         # Pas besoin d'initialiser!

# Avec list (défaut: [])
d = defaultdict(list)
d['items'].append(1)    # Pas d'erreur

# Avec set (défaut: set())
d = defaultdict(set)
d['tags'].add('python')

# Avec str (défaut: '')
d = defaultdict(str)

# Avec lambda
d = defaultdict(lambda: 'N/A')
d = defaultdict(lambda: [])
d = defaultdict(lambda: {'count': 0})

# === COMPARAISON AVEC DICT NORMAL ===

# Dict normal
d = {}
d['count'] = d.get('count', 0) + 1      # Verbeux

# defaultdict
d = defaultdict(int)
d['count'] += 1                          # Simple!

# Dict normal
d = {}
if 'items' not in d:
    d['items'] = []
d['items'].append(1)

# defaultdict
d = defaultdict(list)
d['items'].append(1)                     # Direct!

# === CAS D'USAGE ===

# 1. Grouper par clé
from collections import defaultdict

students = [
    ('Alice', 'Math'),
    ('Bob', 'Physics'),
    ('Charlie', 'Math'),
    ('David', 'Physics')
]

by_subject = defaultdict(list)
for name, subject in students:
    by_subject[subject].append(name)

print(dict(by_subject))
# {'Math': ['Alice', 'Charlie'], 'Physics': ['Bob', 'David']}

# 2. Compter occurrences
text = "hello world hello"
word_count = defaultdict(int)
for word in text.split():
    word_count[word] += 1

# 3. Graph (adjacency list)
graph = defaultdict(list)
graph['A'].append('B')
graph['A'].append('C')
graph['B'].append('C')

# 4. Multidict (plusieurs valeurs par clé)
multidict = defaultdict(list)
multidict['fruit'].append('apple')
multidict['fruit'].append('banana')
multidict['vegetable'].append('carrot')

# 5. Tree structure
def tree():
    return defaultdict(tree)

users = tree()
users['john']['age'] = 30
users['john']['address']['city'] = 'Paris'
users['jane']['age'] = 25

# 6. Index inversé
docs = [
    "Python is great",
    "Python programming",
    "Great programming language"
]

inverted_index = defaultdict(list)
for doc_id, doc in enumerate(docs):
    for word in doc.lower().split():
        inverted_index[word].append(doc_id)

# 7. Accumuler valeurs
sales = [
    ('2024-01', 100),
    ('2024-01', 150),
    ('2024-02', 200),
    ('2024-01', 50)
]

monthly_sales = defaultdict(int)
for month, amount in sales:
    monthly_sales[month] += amount

# 8. Sets de tags
tags_by_post = defaultdict(set)
tags_by_post[1].add('python')
tags_by_post[1].add('tutorial')
tags_by_post[2].add('python')

# === MÉTHODES ===

d = defaultdict(int)
d['a'] = 5

# Comme dict normal
print(d.keys())
print(d.values())
print(d.items())
print(d.get('a'))
d.update({'b': 10})
d.pop('a')
d.clear()

# Accéder à default_factory
print(d.default_factory)        # <class 'int'>

# Changer default_factory
d.default_factory = list
d['new_key'].append(1)

# Désactiver default_factory
d.default_factory = None
# d['autre_key']  # Lèvera KeyError

# === CONVERSION ===

# defaultdict vers dict
dd = defaultdict(int, {'a': 1, 'b': 2})
regular_dict = dict(dd)

# dict vers defaultdict
regular_dict = {'a': 1, 'b': 2}
dd = defaultdict(int, regular_dict)

# === PATTERNS AVANCÉS ===

# Compteur personnalisé
class Counter(defaultdict):
    def __init__(self):
        super().__init__(int)
    
    def increment(self, key, amount=1):
        self[key] += amount

# Nested defaultdict
nested = defaultdict(lambda: defaultdict(int))
nested['user1']['logins'] = 5
nested['user1']['purchases'] = 3

# Dict avec valeurs complexes
from collections import namedtuple
Record = namedtuple('Record', ['count', 'total'])
d = defaultdict(lambda: Record(0, 0))


[OK] ORDEREDDICT - Dict Ordonné


# === NOTE ===
# Depuis Python 3.7+, les dicts normaux gardent l'ordre d'insertion
# OrderedDict reste utile pour:
# - Compatibilité Python < 3.7
# - move_to_end()
# - Égalité sensible à l'ordre

from collections import OrderedDict

# === CRÉATION ===

# Vide
od = OrderedDict()

# Depuis dict
od = OrderedDict({'a': 1, 'b': 2})

# Depuis items
od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])

# Depuis kwargs
od = OrderedDict(a=1, b=2, c=3)

# === OPÉRATIONS ===

od = OrderedDict()
od['a'] = 1
od['b'] = 2
od['c'] = 3

# Ordre est préservé
print(list(od.keys()))      # ['a', 'b', 'c']

# Réordonner: supprimer et rajouter
od['a'] = od.pop('a')       # 'a' va à la fin
print(list(od.keys()))      # ['b', 'c', 'a']

# === MÉTHODE SPÉCIALE ===

od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])

# move_to_end() - déplacer clé
od.move_to_end('a')             # 'a' à la fin
print(list(od.keys()))          # ['b', 'c', 'a']

od.move_to_end('c', last=False) # 'c' au début
print(list(od.keys()))          # ['c', 'b', 'a']

# popitem() - retirer dernier élément (LIFO)
od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
item = od.popitem()             # ('c', 3)
print(list(od.keys()))          # ['a', 'b']

# popitem(last=False) - retirer premier (FIFO)
item = od.popitem(last=False)   # ('a', 1)
print(list(od.keys()))          # ['b']

# === COMPARAISON ===

# OrderedDict compare ordre
od1 = OrderedDict([('a', 1), ('b', 2)])
od2 = OrderedDict([('b', 2), ('a', 1)])
print(od1 == od2)               # False (ordre différent)

# dict normal ignore ordre (Python 3.8+)
d1 = {'a': 1, 'b': 2}
d2 = {'b': 2, 'a': 1}
print(d1 == d2)                 # True

# === CAS D'USAGE ===

# 1. LRU Cache simple
class LRUCache(OrderedDict):
    def __init__(self, capacity):
        self.capacity = capacity
        super().__init__()
    
    def get(self, key):
        if key not in self:
            return -1
        self.move_to_end(key)
        return self[key]
    
    def put(self, key, value):
        if key in self:
            self.move_to_end(key)
        self[key] = value
        if len(self) > self.capacity:
            self.popitem(last=False)

cache = LRUCache(3)
cache.put(1, 'a')
cache.put(2, 'b')
cache.put(3, 'c')
cache.put(4, 'd')   # Évince (1, 'a')

# 2. Trier dict par clés
od = OrderedDict(sorted({'b': 2, 'a': 1, 'c': 3}.items()))

# 3. Trier par valeurs
data = {'Alice': 90, 'Bob': 85, 'Charlie': 95}
sorted_data = OrderedDict(sorted(data.items(), key=lambda x: x[1], reverse=True))

# 4. Queue avec priorités
queue = OrderedDict()
queue['task1'] = 'high'
queue['task2'] = 'low'
queue.move_to_end('task2')  # Basse priorité à la fin


[OK] CHAINMAP - Vue Chainée de Dicts (SUITE)


from collections import ChainMap

# === CAS D'USAGE (suite) ===

# 3. Simulation de portée (scope)
global_vars = {'x': 10, 'y': 20}
local_vars = {'x': 30, 'z': 40}
scope = ChainMap(local_vars, global_vars)
print(scope['x'])       # 30 (local override global)
print(scope['y'])       # 20 (depuis global)
print(scope['z'])       # 40 (seulement dans local)

# 4. Fusion temporaire sans modifier originaux
dict1 = {'name': 'Alice', 'age': 30}
dict2 = {'city': 'Paris', 'country': 'France'}
combined = ChainMap(dict1, dict2)
print(combined['name'], combined['city'])
# dict1 et dict2 restent inchangés

# 5. Configuration en couches (CLI > Env > Config > Defaults)
import os
import argparse

defaults = {
    'host': 'localhost',
    'port': 8000,
    'debug': False,
    'workers': 4,
    'timeout': 30
}

config_file = {
    'port': 9000,
    'workers': 8
}

env_vars = {
    k.lower().replace('app_', ''): v 
    for k, v in os.environ.items() 
    if k.startswith('APP_')
}

parser = argparse.ArgumentParser()
parser.add_argument('--port', type=int)
parser.add_argument('--debug', action='store_true')
args = parser.parse_args()
cli_args = {k: v for k, v in vars(args).items() if v is not None}

# Priorité: CLI > Env > Config > Defaults
config = ChainMap(cli_args, env_vars, config_file, defaults)
print(f"Port: {config['port']}")
print(f"Workers: {config['workers']}")

# 6. Template variables avec fallback
page_vars = {'title': 'Mon Site'}
section_vars = {'title': 'Section 1', 'author': 'Alice'}
global_vars = {'site_name': 'MyWebsite', 'year': 2024}

template_context = ChainMap(section_vars, page_vars, global_vars)
# section_vars['title'] prioritaire sur page_vars['title']

# 7. État de jeu avec overrides
base_stats = {'health': 100, 'mana': 50, 'strength': 10}
equipment_bonus = {'strength': 5, 'defense': 3}
buff_effects = {'strength': 2, 'speed': 5}

player_stats = ChainMap(buff_effects, equipment_bonus, base_stats)
print(f"Strength total: {sum(d.get('strength', 0) for d in player_stats.maps)}")

# === OPÉRATIONS AVANCÉES ===

# Parcourir tous les dicts
cm = ChainMap({'a': 1}, {'b': 2}, {'c': 3})
for i, d in enumerate(cm.maps):
    print(f"Dict {i}: {d}")

# Créer vue sur sous-ensemble
dict1 = {'a': 1, 'b': 2, 'c': 3}
dict2 = {'d': 4, 'e': 5}
dict3 = {'f': 6}

cm_full = ChainMap(dict1, dict2, dict3)
cm_partial = ChainMap(dict1, dict2)  # Sans dict3

# Modifier dict sous-jacent affecte ChainMap
dict1['a'] = 100
print(cm_full['a'])  # 100

# Ajouter contexte temporaire
original_cm = ChainMap(dict1, dict2)
with_temp = original_cm.new_child({'temp': 'value'})
# Utiliser with_temp...
# Revenir à original_cm en utilisant .parents

# === PATTERNS AVANCÉS ===

# Pattern 1: Gestionnaire de contextes
class ContextManager:
    def __init__(self):
        self.contexts = ChainMap({})
    
    def __enter__(self):
        self.contexts = self.contexts.new_child()
        return self.contexts
    
    def __exit__(self, *args):
        self.contexts = self.contexts.parents

ctx_manager = ContextManager()
ctx_manager.contexts['global'] = 'value'

with ctx_manager as ctx:
    ctx['local'] = 'temp'
    print(ctx['global'])  # Accessible
    print(ctx['local'])   # Accessible

# 'local' n'est plus accessible après le with

# Pattern 2: Résolution de variables
class VariableResolver:
    def __init__(self):
        self.scopes = [{}]  # Global scope
    
    def push_scope(self):
        """Nouveau scope (fonction, bloc, etc.)"""
        self.scopes.insert(0, {})
    
    def pop_scope(self):
        """Sortir du scope"""
        if len(self.scopes) > 1:
            self.scopes.pop(0)
    
    def set(self, name, value):
        """Définir dans scope actuel"""
        self.scopes[0][name] = value
    
    def get(self, name):
        """Résoudre variable"""
        chain = ChainMap(*self.scopes)
        return chain.get(name)
    
    def get_all_vars(self):
        """Toutes les variables visibles"""
        return dict(ChainMap(*self.scopes))

resolver = VariableResolver()
resolver.set('x', 10)           # Global
resolver.push_scope()
resolver.set('y', 20)           # Local
resolver.set('x', 30)           # Shadow global x
print(resolver.get('x'))        # 30 (local)
print(resolver.get('y'))        # 20
resolver.pop_scope()
print(resolver.get('x'))        # 10 (global de nouveau visible)
# print(resolver.get('y'))      # None (y n'existe plus)

# Pattern 3: Configuration avec héritage
class ConfigurationChain:
    def __init__(self):
        self.chain = ChainMap()
    
    def load_defaults(self, defaults):
        """Niveau le plus bas"""
        self.chain = ChainMap(self.chain.maps[0] if self.chain.maps else {}, defaults)
    
    def load_file(self, config_dict):
        """Niveau intermédiaire"""
        self.chain = self.chain.new_child(config_dict)
    
    def load_env(self, env_dict):
        """Niveau prioritaire"""
        self.chain = self.chain.new_child(env_dict)
    
    def get(self, key, default=None):
        return self.chain.get(key, default)
    
    def get_source(self, key):
        """Trouver d'où vient une valeur"""
        for i, d in enumerate(self.chain.maps):
            if key in d:
                return i, d[key]
        return None, None

# Pattern 4: Prototype chain (comme JavaScript)
class Prototype:
    def __init__(self, parent=None):
        self.props = {}
        self.parent = parent
    
    def set(self, key, value):
        self.props[key] = value
    
    def get(self, key):
        if self.parent:
            chain = ChainMap(self.props, self.parent.props)
        else:
            chain = ChainMap(self.props)
        return chain.get(key)
    
    def has_own(self, key):
        """Propriété propre (pas héritée)"""
        return key in self.props

# Utilisation
base_vehicle = Prototype()
base_vehicle.set('wheels', 4)
base_vehicle.set('engine', 'combustion')

car = Prototype(parent=base_vehicle)
car.set('doors', 4)

sports_car = Prototype(parent=car)
sports_car.set('engine', 'turbo')  # Override

print(sports_car.get('wheels'))    # 4 (de base_vehicle)
print(sports_car.get('doors'))     # 4 (de car)
print(sports_car.get('engine'))    # 'turbo' (propre, override)

# === COMPARAISON AVEC ALTERNATIVES ===

# ChainMap vs {**dict1, **dict2}
dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}

# Avec merge (copie)
merged = {**dict1, **dict2}
dict1['a'] = 100
print(merged['a'])      # 1 (copie, pas affecté)

# Avec ChainMap (vue)
chain = ChainMap(dict2, dict1)  # Ordre inversé pour même effet
dict1['a'] = 100
print(chain['a'])       # 100 (vue, affecté)

# ChainMap vs dict.update()
d = dict1.copy()
d.update(dict2)
# d est une copie, modifications de dict1/dict2 ne l'affectent pas

# ChainMap garde références, économise mémoire

# === LIMITES ET GOTCHAS ===

# 1. Modifications affectent seulement premier dict
cm = ChainMap({'a': 1}, {'b': 2})
cm['c'] = 3             # Ajoute à maps[0]
cm['b'] = 20            # Ajoute 'b' à maps[0], ne modifie pas maps[1]
print(cm.maps)          # [{'a': 1, 'c': 3, 'b': 20}, {'b': 2}]
print(cm['b'])          # 20 (premier dict prioritaire)

# 2. Suppression
cm = ChainMap({'a': 1}, {'a': 10, 'b': 2})
del cm['a']             # Supprime de maps[0]
print(cm['a'])          # 10 (maps[1] devient visible)
# del cm['a']           # KeyError! Plus dans maps[0]

# 3. Itération peut avoir doublons
cm = ChainMap({'a': 1, 'b': 2}, {'b': 3, 'c': 4})
print(list(cm.keys()))  # ['a', 'b', 'c', 'b'] - 'b' apparaît 2 fois!
print(list(cm))         # ['a', 'b', 'c', 'b']

# Pour clés uniques:
print(list(dict.fromkeys(cm)))  # ['a', 'b', 'c']
# ou
print(list(set(cm.keys())))

# 4. len() compte toutes les clés (avec doublons)
cm = ChainMap({'a': 1}, {'a': 2, 'b': 3})
print(len(cm))          # 3 (pas 2!)

# Pour nombre de clés uniques:
print(len(set(cm.keys())))  # 2

# 5. Performance O(n) pour recherche
# ChainMap cherche dans chaque dict séquentiellement
# Lent si beaucoup de dicts chaînés

# === DÉBOGAGE ET INSPECTION ===

cm = ChainMap({'a': 1, 'b': 2}, {'b': 3, 'c': 4}, {'d': 5})

# Voir tous les dicts
print("Tous les dicts:")
for i, d in enumerate(cm.maps):
    print(f"  maps[{i}]: {d}")

# Trouver où est une clé
def find_key_location(chainmap, key):
    for i, d in enumerate(chainmap.maps):
        if key in d:
            return i, d
    return None, None

idx, dict_ref = find_key_location(cm, 'b')
print(f"'b' trouvé dans maps[{idx}]: {dict_ref}")

# Visualiser cascading
def visualize_chain(chainmap, key):
    print(f"Recherche de '{key}':")
    for i, d in enumerate(chainmap.maps):
        if key in d:
            print(f"  [OK] maps[{i}]: {d[key]} (UTILISÉ)")
        else:
            print(f"  [X] maps[{i}]: (absent)")

visualize_chain(cm, 'b')

# Afficher résolution complète
def show_all_values(chainmap, key):
    values = []
    for i, d in enumerate(chainmap.maps):
        if key in d:
            values.append((i, d[key]))
    return values

print(f"Toutes les valeurs de 'b': {show_all_values(cm, 'b')}")


[OK] COLLECTIONS.ABC - Classes de Base Abstraites


# Les Abstract Base Classes définissent les interfaces des conteneurs

from collections.abc import (
    Iterable, Iterator,
    Sequence, MutableSequence,
    Set, MutableSet,
    Mapping, MutableMapping,
    Container, Sized, Callable
)

# === VÉRIFICATION DE TYPE ===

from collections.abc import Sequence, Mapping

# Vérifier si objet est séquence
print(isinstance([1, 2, 3], Sequence))          # True
print(isinstance((1, 2, 3), Sequence))          # True
print(isinstance("hello", Sequence))            # True
print(isinstance({1, 2, 3}, Sequence))          # False (set)

# Vérifier si objet est mapping
print(isinstance({'a': 1}, Mapping))            # True
print(isinstance([1, 2], Mapping))              # False

# Différence avec types concrets
print(isinstance({'a': 1}, dict))               # True (type exact)
print(isinstance(OrderedDict(), dict))          # False
print(isinstance(OrderedDict(), Mapping))       # True (interface)

# === CRÉER CONTENEURS PERSONNALISÉS ===

# Exemple 1: Séquence personnalisée
from collections.abc import Sequence

class RepeatedSequence(Sequence):
    """Séquence qui répète ses éléments"""
    
    def __init__(self, items, repeat=2):
        self._items = list(items)
        self._repeat = repeat
    
    def __getitem__(self, index):
        actual_len = len(self._items) * self._repeat
        if isinstance(index, slice):
            return [self[i] for i in range(*index.indices(actual_len))]
        if index < 0:
            index += actual_len
        if index < 0 or index >= actual_len:
            raise IndexError("Index out of range")
        return self._items[index % len(self._items)]
    
    def __len__(self):
        return len(self._items) * self._repeat

seq = RepeatedSequence(['a', 'b', 'c'], repeat=3)
print(len(seq))         # 9
print(seq[0])           # 'a'
print(seq[3])           # 'a' (répété)
print(list(seq))        # ['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b', 'c']

# Exemple 2: Mapping personnalisé
from collections.abc import MutableMapping

class CaseInsensitiveDict(MutableMapping):
    """Dict insensible à la casse"""
    
    def __init__(self):
        self._data = {}
    
    def __getitem__(self, key):
        return self._data[key.lower()]
    
    def __setitem__(self, key, value):
        self._data[key.lower()] = value
    
    def __delitem__(self, key):
        del self._data[key.lower()]
    
    def __iter__(self):
        return iter(self._data)
    
    def __len__(self):
        return len(self._data)
    
    def __hash__(self):
        if self._hash is None:
            self._hash = hash(frozenset(self._data.items()))
        return self._hash
    
    def __repr__(self):
        return f"FrozenDict({self._data})"

fd = FrozenDict(a=1, b=2)
# fd['a'] = 10  # AttributeError: pas de __setitem__
print(hash(fd))     # Hashable!

# Utilisable comme clé de dict ou élément de set
cache = {fd: "result"}
print(cache[fd])

# === Recipe 7: Priority Queue avec Counter ===

from collections import Counter, deque
import heapq

class PriorityCounter:
    """Compteur avec traitement par priorité"""
    
    def __init__(self):
        self.counter = Counter()
    
    def add(self, item, count=1):
        self.counter[item] += count
    
    def process_top_n(self, n):
        """Traiter les N items les plus fréquents"""
        top_items = self.counter.most_common(n)
        for item, count in top_items:
            yield item, count
            del self.counter[item]
    
    def process_by_priority(self):
        """Traiter tous par ordre de priorité"""
        while self.counter:
            item, count = self.counter.most_common(1)[0]
            yield item, count
            del self.counter[item]

pc = PriorityCounter()
pc.add('task_a', 5)
pc.add('task_b', 10)
pc.add('task_c', 3)

for task, priority in pc.process_by_priority():
    print(f"Traiter {task} (priorité: {priority})")

# === Recipe 8: Moving Average avec deque ===

from collections import deque
from statistics import mean

class MovingAverage:
    """Moyenne mobile"""
    
    def __init__(self, window_size):
        self.window = deque(maxlen=window_size)
    
    def add(self, value):
        self.window.append(value)
    
    def average(self):
        if not self.window:
            return None
        return mean(self.window)
    
    def is_full(self):
        return len(self.window) == self.window.maxlen

ma = MovingAverage(3)
for val in [10, 20, 30, 40, 50]:
    ma.add(val)
    print(f"Valeur: {val}, Moyenne: {ma.average():.2f}")

# === Recipe 9: Event System avec defaultdict ===

from collections import defaultdict

class EventEmitter:
    """Système d'événements simple"""
    
    def __init__(self):
        self._listeners = defaultdict(list)
    
    def on(self, event, callback):
        """Enregistrer listener"""
        self._listeners[event].append(callback)
    
    def off(self, event, callback):
        """Retirer listener"""
        if event in self._listeners:
            try:
                self._listeners[event].remove(callback)
            except ValueError:
                pass
    
    def emit(self, event, *args, **kwargs):
        """Déclencher événement"""
        for callback in self._listeners[event]:
            callback(*args, **kwargs)
    
    def once(self, event, callback):
        """Listener qui s'exécute une seule fois"""
        def wrapper(*args, **kwargs):
            callback(*args, **kwargs)
            self.off(event, wrapper)
        self.on(event, wrapper)

emitter = EventEmitter()

def on_data(data):
    print(f"Données reçues: {data}")

emitter.on('data', on_data)
emitter.emit('data', "Hello")
emitter.emit('data', "World")

# === Recipe 10: Memoization avec OrderedDict ===

from collections import OrderedDict
from functools import wraps

def memoize_lru(maxsize=128):
    """Décorateur de memoization avec LRU"""
    def decorator(func):
        cache = OrderedDict()
        
        @wraps(func)
        def wrapper(*args):
            if args in cache:
                # Déplacer à la fin (récemment utilisé)
                cache.move_to_end(args)
                return cache[args]
            
            result = func(*args)
            cache[args] = result
            
            if len(cache) > maxsize:
                cache.popitem(last=False)
            
            return result
        
        wrapper.cache = cache
        wrapper.cache_info = lambda: {
            'hits': 0,  # À implémenter
            'misses': 0,
            'size': len(cache),
            'maxsize': maxsize
        }
        wrapper.cache_clear = cache.clear
        
        return wrapper
    return decorator

@memoize_lru(maxsize=100)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(100))

# === Recipe 11: Rate Limiter avec deque ===

from collections import deque
from time import time

class RateLimiter:
    """Limiteur de taux (rate limiting)"""
    
    def __init__(self, max_calls, time_window):
        """
        max_calls: nombre max d'appels
        time_window: fenêtre de temps en secondes
        """
        self.max_calls = max_calls
        self.time_window = time_window
        self.calls = deque()
    
    def is_allowed(self):
        """Vérifie si un appel est autorisé"""
        now = time()
        
        # Retirer appels hors de la fenêtre
        while self.calls and self.calls[0] < now - self.time_window:
            self.calls.popleft()
        
        if len(self.calls) < self.max_calls:
            self.calls.append(now)
            return True
        
        return False
    
    def time_until_allowed(self):
        """Temps à attendre avant prochain appel"""
        if len(self.calls) < self.max_calls:
            return 0
        
        oldest_call = self.calls[0]
        return max(0, self.time_window - (time() - oldest_call))

# Max 5 appels par minute
limiter = RateLimiter(max_calls=5, time_window=60)

for i in range(10):
    if limiter.is_allowed():
        print(f"Appel {i+1}: Autorisé")
    else:
        wait_time = limiter.time_until_allowed()
        print(f"Appel {i+1}: Refusé (attendre {wait_time:.1f}s)")

# === Recipe 12: Undo/Redo Stack ===

from collections import deque

class UndoRedoStack:
    """Système undo/redo avec limite de mémoire"""
    
    def __init__(self, maxlen=100):
        self.undo_stack = deque(maxlen=maxlen)
        self.redo_stack = deque(maxlen=maxlen)
        self.current_state = None
    
    def set_initial_state(self, state):
        self.current_state = state
    
    def execute(self, action, new_state):
        """Exécuter action et sauvegarder état"""
        if self.current_state is not None:
            self.undo_stack.append((action, self.current_state))
        self.current_state = new_state
        self.redo_stack.clear()
    
    def undo(self):
        """Annuler dernière action"""
        if not self.undo_stack:
            return False
        
        action, old_state = self.undo_stack.pop()
        self.redo_stack.append((action, self.current_state))
        self.current_state = old_state
        return True
    
    def redo(self):
        """Refaire action annulée"""
        if not self.redo_stack:
            return False
        
        action, new_state = self.redo_stack.pop()
        self.undo_stack.append((action, self.current_state))
        self.current_state = new_state
        return True
    
    def get_state(self):
        return self.current_state
    
    def can_undo(self):
        return len(self.undo_stack) > 0
    
    def can_redo(self):
        return len(self.redo_stack) > 0

# === Recipe 13: Frequency Table ===

from collections import Counter

class FrequencyTable:
    """Table de fréquences avec statistiques"""
    
    def __init__(self, data=None):
        self.counter = Counter(data) if data else Counter()
    
    def add(self, item, count=1):
        self.counter[item] += count
    
    def frequency(self, item):
        """Fréquence relative"""
        total = sum(self.counter.values())
        return self.counter[item] / total if total > 0 else 0
    
    def percentile(self, item):
        """Percentile (combien d'éléments ont fréquence ≤)"""
        item_freq = self.counter[item]
        below = sum(1 for count in self.counter.values() if count <= item_freq)
        return below / len(self.counter) if self.counter else 0
    
    def mode(self):
        """Mode (valeur la plus fréquente)"""
        if not self.counter:
            return None
        return self.counter.most_common(1)[0][0]
    
    def median_class(self):
        """Classe médiane"""
        total = sum(self.counter.values())
        if total == 0:
            return None
        
        cumulative = 0
        for item, count in self.counter.most_common()[::-1]:
            cumulative += count
            if cumulative >= total / 2:
                return item
    
    def relative_frequencies(self):
        """Toutes les fréquences relatives"""
        total = sum(self.counter.values())
        return {item: count/total for item, count in self.counter.items()}
    
    def cumulative_frequencies(self):
        """Fréquences cumulatives"""
        sorted_items = sorted(self.counter.items(), key=lambda x: x[0])
        cumulative = 0
        result = {}
        for item, count in sorted_items:
            cumulative += count
            result[item] = cumulative
        return result

ft = FrequencyTable([1, 2, 2, 3, 3, 3, 4, 4, 4, 4])
print(f"Mode: {ft.mode()}")
print(f"Fréquence de 3: {ft.frequency(3):.2%}")
print(f"Fréquences relatives: {ft.relative_frequencies()}")

# === Recipe 14: Time Series Buffer ===

from collections import deque, namedtuple
from datetime import datetime, timedelta

TimePoint = namedtuple('TimePoint', ['timestamp', 'value'])

class TimeSeriesBuffer:
    """Buffer pour séries temporelles avec fenêtre glissante"""
    
    def __init__(self, time_window):
        """
        time_window: timedelta ou secondes
        """
        if isinstance(time_window, (int, float)):
            time_window = timedelta(seconds=time_window)
        self.time_window = time_window
        self.data = deque()
    
    def add(self, value, timestamp=None):
        """Ajouter point de données"""
        if timestamp is None:
            timestamp = datetime.now()
        
        self.data.append(TimePoint(timestamp, value))
        self._cleanup()
    
    def _cleanup(self):
        """Retirer données hors fenêtre"""
        now = datetime.now()
        cutoff = now - self.time_window
        
        while self.data and self.data[0].timestamp < cutoff:
            self.data.popleft()
    
    def get_values(self):
        """Valeurs dans la fenêtre"""
        self._cleanup()
        return [point.value for point in self.data]
    
    def average(self):
        """Moyenne dans la fenêtre"""
        values = self.get_values()
        return sum(values) / len(values) if values else 0
    
    def min(self):
        values = self.get_values()
        return min(values) if values else None
    
    def max(self):
        values = self.get_values()
        return max(values) if values else None
    
    def count(self):
        self._cleanup()
        return len(self.data)

# Buffer de 5 minutes
ts = TimeSeriesBuffer(time_window=timedelta(minutes=5))
ts.add(10)
ts.add(20)
ts.add(30)
print(f"Moyenne: {ts.average()}")
print(f"Nombre: {ts.count()}")

# === Recipe 15: Bloom Filter (approximation) ===

from collections import Counter
import hashlib

class SimpleBloomFilter:
    """Bloom filter simple avec Counter"""
    
    def __init__(self, size=1000, hash_count=3):
        self.size = size
        self.hash_count = hash_count
        self.bits = Counter()
    
    def _hashes(self, item):
        """Générer plusieurs hashes"""
        hashes = []
        for i in range(self.hash_count):
            h = hashlib.md5(f"{item}{i}".encode()).hexdigest()
            hashes.append(int(h, 16) % self.size)
        return hashes
    
    def add(self, item):
        """Ajouter élément"""
        for h in self._hashes(item):
            self.bits[h] = 1
    
    def might_contain(self, item):
        """Vérifie si élément pourrait être présent"""
        return all(self.bits[h] for h in self._hashes(item))
    
    def __contains__(self, item):
        return self.might_contain(item)

bf = SimpleBloomFilter()
bf.add("alice")
bf.add("bob")
print("alice" in bf)    # True
print("charlie" in bf)  # False (probablement)
# Peut avoir faux positifs, jamais de faux négatifs


[OK] OPTIMISATION ET PERFORMANCE


# === Choisir le bon conteneur ===

# Pour comptages:
from collections import Counter
words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
counts = Counter(words)  # Plus rapide que dict manuel

# Pour groupements:
from collections import defaultdict
by_category = defaultdict(list)  # Plus rapide que dict avec vérifications

# Pour files:
from collections import deque
queue = deque()  # O(1) pour append/popleft vs O(n) pour list

# === Benchmark comparatif ===

import timeit
from collections import deque, defaultdict, Counter

# deque vs list pour queue
list_queue = timeit.timeit(
    'q.append(1); q.pop(0)',
    setup='q = []',
    number=10000
)

deque_queue = timeit.timeit(
    'q.append(1); q.popleft()',
    setup='from collections import deque; q = deque()',
    number=10000
)

print(f"list queue: {list_queue:.4f}s")
print(f"deque queue: {deque_queue:.4f}s")
print(f"deque est {list_queue/deque_queue:.1f}x plus rapide")

# defaultdict vs dict avec setdefault
dict_setdefault = timeit.timeit(
    'd.setdefault(1, []).append(2)',
    setup='d = {}',
    number=100000
)

defaultdict_time = timeit.timeit(
    'd[1].append(2)',
    setup='from collections import defaultdict; d = defaultdict(list)',
    number=100000
)

print(f"\ndict.setdefault: {dict_setdefault:.4f}s")
print(f"defaultdict: {defaultdict_time:.4f}s")
print(f"defaultdict est {dict_setdefault/defaultdict_time:.1f}x plus rapide")

# === Économie mémoire ===

import sys
from collections import namedtuple

# namedtuple vs dict
Point = namedtuple('Point', ['x', 'y', 'z'])
point_tuple = Point(1, 2, 3)
point_dict = {'x': 1, 'y': 2, 'z': 3}

print(f"\nnamedtuple: {sys.getsizeof(point_tuple)} bytes")
print(f"dict: {sys.getsizeof(point_dict)} bytes")

# Pour 1000 points:
points_tuple = [Point(i, i+1, i+2) for i in range(1000)]
points_dict = [{'x': i, 'y': i+1, 'z': i+2} for i in range(1000)]

size_tuple = sum(sys.getsizeof(p) for p in points_tuple)
size_dict = sum(sys.getsizeof(p) for p in points_dict)

print(f"\n1000 namedtuples: {size_tuple} bytes")
print(f"1000 dicts: {size_dict} bytes")
print(f"Économie: {(1 - size_tuple/size_dict)*100:.1f}%")

# === Patterns pour performance ===

# 1. Réutiliser Counter plutôt que recréer
counter = Counter()
for batch in data_batches:
    counter.update(batch)  # Plus efficace que Counter(batch) à chaque fois

# 2. Utiliser most_common avec limite
from collections import Counter
data = range(1000000)
top_10 = Counter(data).most_common(10)  # Efficace avec heapq

# 3. deque rotate pour buffer circulaire
from collections import deque
buffer = deque([1, 2, 3, 4, 5], maxlen=5)
buffer.rotate(1)  # O(1) vs reconstruire liste

# 4. ChainMap pour configs (pas de copie)
from collections import ChainMap
config = ChainMap(overrides, defaults)  # Vue, pas copie


[OK] INTÉGRATION AVEC AUTRES MODULES


# === avec itertools ===

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

# Counter avec islice (premiers N éléments)
counter = Counter(islice(large_iterable, 1000))

# defaultdict avec groupby
data = [('fruit', 'apple'), ('veg', 'carrot'), ('fruit', 'banana')]
by_type = defaultdict(list)
for key, group in groupby(sorted(data), key=lambda x: x[0]):
    by_type[key].extend(item[1] for item in group)

# deque avec chain (concaténer iterables)
d1 = deque([1, 2, 3])
d2 = deque([4, 5, 6])
combined = deque(chain(d1, d2))

# === avec functools ===

from collections import OrderedDict
from functools import lru_cache, wraps

# LRU cache personnalisé
@lru_cache(maxsize=128)
def expensive_function(n):
    return sum(range(n))

# Combiner avec OrderedDict pour inspection
class InspectableCache:
    def __init__(self, maxsize=128):
        self.cache = OrderedDict()
        self.maxsize = maxsize
        self.hits = 0
        self.misses = 0
    
    def __call__(self, func):
        @wraps(func)
        def wrapper(*args):
            if args in self.cache:
                self.hits += 1
                self.cache.move_to_end(args)
                return self.cache[args]
            
            self.misses += 1
            result = func(*args)
            self.cache[args] = result
            
            if len(self.cache) > self.maxsize:
                self.cache.popitem(last=False)
            
            return result
        return wrapper

# === avec typing ===

from typing import Counter as CounterType, DefaultDict, Deque
from collections import Counter, defaultdict, deque

def count_words(text: str) -> CounterType[str]:
    return Counter(text.split())

def group_items(items: list[tuple]) -> DefaultDict[str, list]:
    groups = defaultdict(list)
    for key, value in items:
        groups[key].append(value)
    return groups

def create_queue(items: list[int]) -> Deque[int]:
    return deque(items)

# === avec dataclasses (Python 3.7+) ===

from dataclasses import dataclass
from collections import defaultdict, Counter

@dataclass
class Stats:
    counter: Counter
    groups: defaultdict
    
    def total_count(self):
        return sum(self.counter.values())
    
    def group_counts(self):
        return {k: len(v) for k, v in self.groups.items()}

# === avec json ===

import json
from collections import OrderedDict, defaultdict, Counter

# OrderedDict avec JSON
data = OrderedDict([('name', 'Alice'), ('age', 30), ('city', 'Paris')])
json_str = json.dumps(data)
loaded = json.loads(json_str, object_pairs_hook=OrderedDict)

# Counter vers JSON
counter = Counter(['a', 'b', 'a', 'c', 'b', 'a'])
json_str = json.dumps(dict(counter))

# defaultdict vers JSON
dd = defaultdict(list, {'a': [1, 2], 'b': [3, 4]})
json_str = json.dumps(dict(dd))

# === avec pickle ===

import pickle
from collections import *

# Tous les collections sont picklable
data = {
    'counter': Counter(['a', 'b', 'c']),
    'deque': deque([1, 2, 3]),
    'defaultdict': defaultdict(list),
    'chainmap': ChainMap({'a': 1}, {'b': 2})
}

pickled = pickle.dumps(data)
restored = pickle.loads(pickled)


[OK] TESTS ET DEBUGGING


# === Tests unitaires ===

import unittest
from collections import Counter, defaultdict, deque

class TestCollections(unittest.TestCase):
    
    def test_counter_basic(self):
        c = Counter(['a', 'b', 'a', 'c'])
        self.assertEqual(c['a'], 2)
        self.assertEqual(c.most_common(1)[0], ('a', 2))
    
    def test_deque_maxlen(self):
        d = deque(maxlen=3)
        d.extend([1, 2, 3, 4])
        self.assertEqual(list(d), [2, 3, 4])
    
    def test_defaultdict_groups(self):
        dd = defaultdict(list)
        dd['a'].append(1)
        dd['a'].append(2)
        self.assertEqual(dd['a'], [1, 2])
        self.assertEqual(dd['b'], [])  # Crée liste vide
    
    def test_chainmap_precedence(self):
        cm = ChainMap({'a': 1}, {'a': 2, 'b': 3})
        self.assertEqual(cm['a'], 1)  # Premier dict prioritaire
        self.assertEqual(cm['b'], 3)

# === Debugging helpers ===

from collections import Counter, defaultdict
import pprint

def debug_counter(counter, label="Counter"):
    """Afficher Counter de façon lisible"""
    print(f"\n{label}:")
    print(f"  Total items: {sum(counter.values())}")
    print(f"  Unique items: {len(counter)}")
    print(f"  Top 5:")
    for item, count in counter.most_common(5):
        print(f"    {item}: {count}")

def debug_defaultdict(dd, label="DefaultDict"):
    """Afficher defaultdict"""
    print(f"\n{label}:")
    for key, value in sorted(dd.items()):
        if isinstance(value, list):
            print(f"  {key}: {len(value)} items")
        else:
            print(f"  {key}: {value}")

def debug_chainmap(cm, label="ChainMap"):
    """Afficher ChainMap avec tous les dicts"""
    print(f"\n{label}:")
    for i, d in enumerate(cm.maps):
        print(f"  Level {i}: {d}")
    print(f"  Combined view: {dict(cm)}")

# === Profiling ===

import cProfile
from collections import Counter, deque

def profile_counter():
    """Profiler opérations Counter"""
    data = list(range(10000)) * 100
    c = Counter(data)
    top = c.most_common(10)

def profile_deque():
    """Profiler opérations deque"""
    d = deque()
    for i in range(10000):
        d.append(i)
        if i % 2 == 0:
            d.popleft()

# Profiler
cProfile.run('profile_counter()')
cProfile.run('profile_deque()')

# === Validation ===

def validate_counter(counter):
    """Valider intégrité Counter"""
    assert all(isinstance(k, Hashable) for k in counter.keys()), "Clés non-hashable"
    assert all(isinstance(v, (int, float)) for v in counter.values()), "Valeurs non-numériques"
    assert all(v >= 0 for v in counter.values()), "Comptes négatifs"
    return True

def validate_defaultdict(dd, expected_type):
    """Valider defaultdict"""
    assert dd.default_factory is not None, "default_factory est None"
    for value in dd.values():
        assert isinstance(value, expected_type), f"Valeur type incorrect: {type(value)}"
    return True


[OK] MIGRATION ET COMPATIBILITÉ


# === Python 2 vs Python 3 ===

# Python 2.7
from collections import OrderedDict  # Nécessaire
d = OrderedDict([('a', 1), ('b', 2)])

# Python 3.7+
d = {'a': 1, 'b': 2}  # Dict garde ordre automatiquement

# Mais OrderedDict toujours utile pour:
# - move_to_end()
# - Égalité sensible à l'ordre
# - Compatibilité

# === Migrer de dict vers defaultdict ===

# Avant
d = {}
for item in items:
    if item.category not in d:
        d[item.category] = []
    d[item.category].append(item)

# Après
from collections import defaultdict
d = defaultdict(list)
for item in items:
    d[item.category].append(item)

# === Migrer de list vers deque ===

# Avant (lent pour queue)
queue = []
queue.append(item)
first = queue.pop(0)  # O(n)

# Après (rapide)
from collections import deque
queue = deque()
queue.append(item)
first = queue.popleft()  # O(1)

# === Migrer de namedtuple vers dataclass ===

# Avant
from collections import namedtuple
Person = namedtuple('Person', ['name', 'age'])
p = Person('Alice', 30)
# p.age = 31  # Erreur: immutable

# Après (Python 3.7+)
from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int

p = Person('Alice', 30)
p.age = 31  # OK: mutable par défaut

# Pour immutable avec dataclass:
@dataclass(frozen=True)
class Person:
    name: str
    age: int


[OK] RESSOURCES ET DOCUMENTATION


# === Documentation officielle ===
# https://docs.python.org/3/library/collections.html
# https://docs.python.org/3/library/collections.abc.html

# === PEPs pertinents ===
# PEP 234 -- Iterators
# PEP 3106 -- Revamping dict.keys(), .values() and .items()
# PEP 584 -- Add Union Operators To dict (Python 3.9+)

# === Livres recommandés ===
# - "Fluent Python" par Luciano Ramalho
# - "Python Cookbook" par David Beazley
# - "Effective Python" par Brett Slatkin

# === Modules complémentaires ===
import heapq          # Priority queues
import bisect         # Array bisection
import array          # Efficient arrays
import weakref        # Weak references
from collections.abc import * # ABCs

# === Alternatives tierces ===
# sortedcontainers: sorted list, sorted dict, sorted set
# blist: faster list operations
# toolz: functional programming utils
# more-itertools: extensions à itertools


[OK] CHECKLIST POUR CHOISIR LE BON CONTENEUR


"""
Q: Besoin de compter des occurrences?
-> Counter

Q: Besoin de valeur par défaut pour clés manquantes?
-> defaultdict

Q: Besoin d'ajouts/retraits rapides aux deux extrémités?
-> deque

Q: Besoin de tuple avec champs nommés et immutable?
-> namedtuple

Q: Besoin d'ordre d'insertion et move_to_end()?
-> OrderedDict

Q: Besoin de fusionner plusieurs dicts sans copier?
-> ChainMap

Q: Besoin de créer sous-classe de dict/list/str?
-> UserDict/UserList/UserString

Q: Besoin de vérifier types de conteneurs génériques?
-> collections.abc

Q: Besoin de structure personnalisée?
-> Hériter de collections.abc classes
"""


[OK] RÉSUMÉ DES COMPLEXITÉS


"""
COUNTER
- Création: O(n)
- Accès: O(1)
- most_common(k): O(n log k)
- Opérations (+, -, &, |): O(len(c1) + len(c2))

DEQUE
- append/appendleft: O(1)
- pop/popleft: O(1)
- Index access: O(n)
- rotate: O(k)
- extend/extendleft: O(k)

DEFAULTDICT
- Même que dict: O(1) average pour get/set/del

ORDEREDDICT
- get/set/del: O(1)
- move_to_end: O(1)
- popitem: O(1)

CHAINMAP
- get/contains: O(m) où m = nombre de dicts
- set/del: O(1) (premier dict seulement)
- new_child: O(1)

NAMEDTUPLE
- Création: O(1)
- Accès: O(1)
- _replace: O(n) où n = nombre de champs
"""


[OK] ANTI-PATTERNS À ÉVITER


# === Anti-pattern 1: Utiliser list comme queue ===

# [X] MAUVAIS: O(n) pour pop(0)
queue = []
queue.append(item)
first = queue.pop(0)  # Lent!

# [OK] BON: O(1) avec deque
from collections import deque
queue = deque()
queue.append(item)
first = queue.popleft()  # Rapide!

# === Anti-pattern 2: Dict manuel pour comptage ===

# [X] MAUVAIS: verbeux et sujet aux erreurs
counts = {}
for word in words:
    if word in counts:
        counts[word] += 1
    else:
        counts[word] = 1

# [OK] BON: Counter fait ça mieux
from collections import Counter
counts = Counter(words)

# === Anti-pattern 3: Vérifier clé avant ajout à liste ===

# [X] MAUVAIS: vérifications répétitives
groups = {}
for item in items:
    key = item.category
    if key not in groups:
        groups[key] = []
    groups[key].append(item)

# [OK] BON: defaultdict élimine les vérifications
from collections import defaultdict
groups = defaultdict(list)
for item in items:
    groups[item.category].append(item)

# === Anti-pattern 4: Recréer Counter dans boucle ===

# [X] MAUVAIS: inefficace
for batch in batches:
    counts = Counter(batch)  # Recrée à chaque fois
    process(counts)

# [OK] BON: réutiliser et mettre à jour
from collections import Counter
counts = Counter()
for batch in batches:
    counts.update(batch)
    process(counts)

# === Anti-pattern 5: Accès par index sur deque ===

# [X] MAUVAIS: O(n) pour accès par index
from collections import deque
d = deque(range(1000))
for i in range(len(d)):
    print(d[i])  # Lent!

# [OK] BON: itérer directement
for item in d:
    print(item)  # Rapide!

# Ou utiliser list si accès par index fréquent
lst = list(range(1000))
for i in range(len(lst)):
    print(lst[i])  # O(1)

# === Anti-pattern 6: Modifier ChainMap et s'attendre à modifier tous les dicts ===

# [X] MAUVAIS: penser que modifications affectent tous
from collections import ChainMap
d1 = {'a': 1}
d2 = {'b': 2}
cm = ChainMap(d1, d2)
cm['b'] = 20  # Ajoute 'b' à d1, ne modifie PAS d2!
print(d2['b'])  # Toujours 2

# [OK] BON: comprendre que seul premier dict est modifié
# Ou modifier dict sous-jacent directement
d2['b'] = 20

# === Anti-pattern 7: Counter avec valeurs non-entières ===

# [X] MAUVAIS: utiliser Counter pour moyennes
from collections import Counter
c = Counter()
c['item'] = 3.5  # OK techniquement mais...
c['item'] += 0.7  # ...pas l'usage prévu

# [OK] BON: utiliser dict normal pour valeurs non-compteurs
values = {}
values['item'] = 3.5
values['item'] += 0.7

# === Anti-pattern 8: Oublier que namedtuple est immutable ===

# [X] MAUVAIS: tenter de modifier
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
# p.x = 10  # AttributeError!

# [OK] BON: utiliser _replace()
p = p._replace(x=10)

# Ou utiliser dataclass si mutabilité nécessaire
from dataclasses import dataclass
@dataclass
class Point:
    x: int
    y: int

p = Point(1, 2)
p.x = 10  # OK

# === Anti-pattern 9: Ne pas gérer les valeurs par défaut de defaultdict ===

# [X] MAUVAIS: defaultdict crée valeurs même si non voulu
from collections import defaultdict
d = defaultdict(list)
if 'key' in d:  # Ne PAS faire: if d['key']
    process(d['key'])

# [OK] BON: utiliser 'in' pour vérifier sans créer
if 'key' in d:
    process(d['key'])

# Ou désactiver default_factory temporairement
d.default_factory = None
value = d.get('key')  # Retourne None si absent
d.default_factory = list

# === Anti-pattern 10: Utiliser OrderedDict alors que dict suffit (Python 3.7+) ===

# [X] MAUVAIS (Python 3.7+): overhead inutile
from collections import OrderedDict
d = OrderedDict([('a', 1), ('b', 2)])

# [OK] BON (Python 3.7+): dict garde ordre
d = {'a': 1, 'b': 2}

# Mais OrderedDict toujours utile pour:
od = OrderedDict([('a', 1), ('b', 2)])
od.move_to_end('a')  # Fonctionnalité unique
od == OrderedDict([('b', 2), ('a', 1)])  # Égalité sensible à l'ordre


[OK] EXEMPLES COMPLETS ET RÉALISTES


# === Exemple 1: Système de Cache Multi-niveaux ===

from collections import OrderedDict, ChainMap
from time import time

class MultiLevelCache:
    """Cache avec mémoire L1/L2/L3"""
    
    def __init__(self, l1_size=10, l2_size=100, l3_size=1000):
        self.l1 = OrderedDict()  # Plus rapide
        self.l2 = OrderedDict()  # Moyen
        self.l3 = OrderedDict()  # Plus lent mais plus grand
        
        self.l1_size = l1_size
        self.l2_size = l2_size
        self.l3_size = l3_size
        
        self.chain = ChainMap(self.l1, self.l2, self.l3)
        
        self.hits = {'l1': 0, 'l2': 0, 'l3': 0}
        self.misses = 0
    
    def get(self, key):
        # Chercher dans L1
        if key in self.l1:
            self.hits['l1'] += 1
            self.l1.move_to_end(key)
            return self.l1[key]
        
        # Chercher dans L2
        if key in self.l2:
            self.hits['l2'] += 1
            value = self.l2.pop(key)
            self._add_to_l1(key, value)
            return value
        
        # Chercher dans L3
        if key in self.l3:
            self.hits['l3'] += 1
            value = self.l3.pop(key)
            self._add_to_l1(key, value)
            return value
        
        self.misses += 1
        return None
    
    def set(self, key, value):
        self._add_to_l1(key, value)
    
    def _add_to_l1(self, key, value):
        if key in self.l1:
            self.l1.move_to_end(key)
        else:
            self.l1[key] = value
            if len(self.l1) > self.l1_size:
                evicted_key, evicted_value = self.l1.popitem(last=False)
                self._add_to_l2(evicted_key, evicted_value)
    
    def _add_to_l2(self, key, value):
        self.l2[key] = value
        if len(self.l2) > self.l2_size:
            evicted_key, evicted_value = self.l2.popitem(last=False)
            self._add_to_l3(evicted_key, evicted_value)
    
    def _add_to_l3(self, key, value):
        self.l3[key] = value
        if len(self.l3) > self.l3_size:
            self.l3.popitem(last=False)
    
    def stats(self):
        total_hits = sum(self.hits.values())
        total = total_hits + self.misses
        return {
            'hit_rate': total_hits / total if total > 0 else 0,
            'l1_hits': self.hits['l1'],
            'l2_hits': self.hits['l2'],
            'l3_hits': self.hits['l3'],
            'misses': self.misses
        }

# === Exemple 2: Analyseur de Logs Web ===

from collections import Counter, defaultdict, deque, namedtuple
from datetime import datetime, timedelta

LogEntry = namedtuple('LogEntry', [
    'timestamp', 'ip', 'method', 'path', 'status', 'size', 'user_agent'
])

class WebLogAnalyzer:
    def __init__(self, time_window=timedelta(minutes=5)):
        self.time_window = time_window
        self.recent_logs = deque()
        
        self.status_counts = Counter()
        self.path_counts = Counter()
        self.ip_requests = defaultdict(list)
        self.error_logs = []
    
    def add_log(self, log_entry):
        # Ajouter à deque
        self.recent_logs.append(log_entry)
        
        # Nettoyer anciennes entrées
        cutoff = datetime.now() - self.time_window
        while self.recent_logs and self.recent_logs[0].timestamp < cutoff:
            old_log = self.recent_logs.popleft()
            self._remove_from_counters(old_log)
        
        # Mettre à jour statistiques
        self.status_counts[log_entry.status] += 1
        self.path_counts[log_entry.path] += 1
        self.ip_requests[log_entry.ip].append(log_entry.timestamp)
        
        # Tracker erreurs
        if log_entry.status >= 400:
            self.error_logs.append(log_entry)
    
    def _remove_from_counters(self, log_entry):
        self.status_counts[log_entry.status] -= 1
        if self.status_counts[log_entry.status] <= 0:
            del self.status_counts[log_entry.status]
        
        self.path_counts[log_entry.path] -= 1
        if self.path_counts[log_entry.path] <= 0:
            del self.path_counts[log_entry.path]
    
    def top_paths(self, n=10):
        return self.path_counts.most_common(n)
    
    def error_rate(self):
        total = sum(self.status_counts.values())
        errors = sum(count for status, count in self.status_counts.items() 
                    if status >= 400)
        return errors / total if total > 0 else 0
    
    def detect_suspicious_ips(self, threshold=100):
        """IPs avec trop de requêtes"""
        suspicious = []
        for ip, timestamps in self.ip_requests.items():
            if len(timestamps) > threshold:
                suspicious.append((ip, len(timestamps)))
        return sorted(suspicious, key=lambda x: x[1], reverse=True)
    
    def report(self):
        return {
            'total_requests': len(self.recent_logs),
            'top_5_paths': self.top_paths(5),
            'status_distribution': dict(self.status_counts.most_common()),
            'error_rate': f"{self.error_rate():.2%}",
            'recent_errors': len(self.error_logs),
            'suspicious_ips': self.detect_suspicious_ips()[:5]
        }

# === Exemple 3: Job Scheduler avec Priorités ===

from collections import deque, namedtuple, Counter
from enum import IntEnum
import heapq

class Priority(IntEnum):
    LOW = 3
    MEDIUM = 2
    HIGH = 1
    CRITICAL = 0

Job = namedtuple('Job', ['id', 'priority', 'task', 'timestamp'])

class PriorityJobScheduler:
    def __init__(self):
        self.job_queues = {
            Priority.CRITICAL: deque(),
            Priority.HIGH: deque(),
            Priority.MEDIUM: deque(),
            Priority.LOW: deque()
        }
        
        self.job_counter = Counter()
        self.completed = []
        self.job_id = 0
    
    def add_job(self, task, priority=Priority.MEDIUM):
        job = Job(
            id=self.job_id,
            priority=priority,
            task=task,
            timestamp=datetime.now()
        )
        self.job_id += 1
        
        self.job_queues[priority].append(job)
        self.job_counter[priority] += 1
        return job.id
    
    def get_next_job(self):
        """Récupère job avec plus haute priorité"""
        for priority in [Priority.CRITICAL, Priority.HIGH, 
                        Priority.MEDIUM, Priority.LOW]:
            if self.job_queues[priority]:
                job = self.job_queues[priority].popleft()
                self.job_counter[priority] -= 1
                return job
        return None
    
    def execute_batch(self, batch_size=10):
        """Exécute batch de jobs"""
        executed = []
        for _ in range(batch_size):
            job = self.get_next_job()
            if not job:
                break
            
            # Exécuter job
            result = job.task()
            executed.append((job, result))
            self.completed.append(job)
        
        return executed
    
    def stats(self):
        return {
            'pending_by_priority': dict(self.job_counter),
            'total_pending': sum(self.job_counter.values()),
            'total_completed': len(self.completed),
            'queue_lengths': {
                priority: len(queue) 
                for priority, queue in self.job_queues.items()
            }
        }

# === Exemple 4: Text Search Engine Simple ===

from collections import defaultdict, Counter
import re

class SimpleSearchEngine:
    def __init__(self):
        self.inverted_index = defaultdict(set)  # mot -> set de doc_ids
        self.documents = {}  # doc_id -> texte
        self.doc_word_counts = {}  # doc_id -> Counter
        self.doc_id = 0
    
    def add_document(self, text):
        doc_id = self.doc_id
        self.doc_id += 1
        
        self.documents[doc_id] = text
        
        # Tokenizer simple
        words = re.findall(r'\w+', text.lower())
        word_counts = Counter(words)
        self.doc_word_counts[doc_id] = word_counts
        
        # Construire index inversé
        for word in word_counts:
            self.inverted_index[word].add(doc_id)
        
        return doc_id
    
    def search(self, query):
        """Recherche simple"""
        query_words = re.findall(r'\w+', query.lower())
        
        if not query_words:
            return []
        
        # Trouver documents contenant tous les mots
        result_docs = self.inverted_index[query_words[0]].copy()
        for word in query_words[1:]:
            result_docs &= self.inverted_index[word]
        
        # Scorer par fréquence
        scored = []
        for doc_id in result_docs:
            score = sum(
                self.doc_word_counts[doc_id][word] 
                for word in query_words
            )
            scored.append((doc_id, score, self.documents[doc_id]))
        
        return sorted(scored, key=lambda x: x[1], reverse=True)
    
    def get_word_stats(self):
        """Statistiques sur les mots"""
        all_words = Counter()
        for counts in self.doc_word_counts.values():
            all_words.update(counts)
        
        return {
            'total_words': sum(all_words.values()),
            'unique_words': len(all_words),
            'top_words': all_words.most_common(10)
        }

# === Exemple 5: Session Manager ===

from collections import OrderedDict
from time import time
from secrets import token_hex

class SessionManager:
    def __init__(self, max_sessions=1000, session_timeout=3600):
        self.sessions = OrderedDict()
        self.max_sessions = max_sessions
        self.session_timeout = session_timeout
    
    def create_session(self, user_id, data=None):
        """Créer nouvelle session"""
        session_id = token_hex(16)
        
        self.sessions[session_id] = {
            'user_id': user_id,
            'data': data or {},
            'created_at': time(),
            'last_accessed': time()
        }
        
        # Éviter overflow
        if len(self.sessions) > self.max_sessions:
            self.sessions.popitem(last=False)
        
        return session_id
    
    def get_session(self, session_id):
        """Récupérer session"""
        if session_id not in self.sessions:
            return None
        
        session = self.sessions[session_id]
        
        # Vérifier expiration
        if time() - session['last_accessed'] > self.session_timeout:
            del self.sessions[session_id]
            return None
        
        # Mettre à jour last_accessed et déplacer à la fin
        session['last_accessed'] = time()
        self.sessions.move_to_end(session_id)
        
        return session
    
    def update_session(self, session_id, data):
        """Mettre à jour données de session"""
        session = self.get_session(session_id)
        if session:
            session['data'].update(data)
            return True
        return False
    
    def delete_session(self, session_id):
        """Supprimer session"""
        return self.sessions.pop(session_id, None) is not None
    
    def cleanup_expired(self):
        """Nettoyer sessions expirées"""
        now = time()
        expired = [
            sid for sid, session in self.sessions.items()
            if now - session['last_accessed'] > self.session_timeout
        ]
        for sid in expired:
            del self.sessions[sid]
        return len(expired)
    
    def get_active_users(self):
        """Compter utilisateurs actifs"""
        user_ids = Counter(
            session['user_id'] 
            for session in self.sessions.values()
        )
        return dict(user_ids)


[OK] QUIZ ET EXERCICES


"""
=== QUIZ ===

1. Quelle est la complexité de deque.popleft() ?
   a) O(1)   [OK]
   b) O(n)
   c) O(log n)

2. Counter hérite de quelle classe ?
   a) list
   b) dict   [OK]
   c) set

3. Dans ChainMap({'a': 1}, {'a': 2}), que vaut cm['a'] ?
   a) 1      [OK] (premier dict prioritaire)
   b) 2
   c) [1, 2]

4. namedtuple est-il mutable ?
   a) Oui
   b) Non    [OK]

5. defaultdict(list)['new_key'] retourne quoi ?
   a) KeyError
   b) None
   c) []     [OK]

=== EXERCICES ===

Exercice 1: Implémenter LFU Cache (Least Frequently Used)
Utilisez: Counter, OrderedDict

Exercice 2: Word Ladder (BFS sur mots)
Utilisez: deque, defaultdict

Exercice 3: Top K éléments fréquents dans stream
Utilisez: Counter, heapq

Exercice 4: Système de tags hiérarchiques
Utilisez: defaultdict, ChainMap

Exercice 5: Fenêtre glissante médiane
Utilisez: deque, bisect
"""


[OK] AIDE-MÉMOIRE RAPIDE


"""
┌─────────────────────────────────────────────────────────────┐
│ COLLECTIONS PYTHON - AIDE-MÉMOIRE                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│ Counter          -> Compter occurrences                     │
│   .most_common(n)  -> Top N éléments                        │
│   c1 + c2          -> Additionner compteurs                 │
│   c1 & c2          -> Intersection (min)                    │
│                                                             │
│ deque            -> File/Pile rapide                        │
│   .append(x)       -> Ajouter à droite                      │
│   .appendleft(x)   -> Ajouter à gauche                      │
│   .pop()           -> Retirer à droite                      │
│   .popleft()       -> Retirer à gauche                      │
│   .rotate(n)       -> Rotation                              │
│                                                             │
│ defaultdict      -> Dict avec défaut                        │
│   defaultdict(list)      -> [] par défaut                   │
│   defaultdict(int)       -> 0 par défaut                    │
│   defaultdict(lambda: x) -> x par défaut                    │
│                                                             │
│ OrderedDict      -> Dict ordonné                            │
│   .move_to_end(k)  -> Déplacer clé                          │
│   .popitem()       -> Retirer dernier (LIFO)                │
│                                                             │
│ ChainMap         -> Fusionner dicts                         │
│   ChainMap(d1,d2)  -> d1 prioritaire sur d2                 │
│   .new_child(d)    -> Ajouter dict au début                 │
│   .parents         -> Retirer premier dict                  │
│                                                             │
│ namedtuple       -> Tuple nommé                             │
│   Point('Point', 'x y')  -> Créer                           │
│   p._replace(x=10)       -> Modifier (nouveau tuple)        │
│   p._asdict()            -> Vers dict                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
"""