# Fichier: python_cheats/cheatsheets/itertools.txt
# Cheatsheet itertools Python - Guide Complet


[OK] INTRODUCTION

# itertools fournit des itérateurs pour créer des boucles efficaces
# Inspiré par APL, Haskell et SML
# Tous les outils retournent des itérateurs (lazy evaluation)

from itertools import *


[OK] ITÉRATEURS INFINIS


# count(start=0, step=1) - Compteur infini
for i in count(10):
    print(i)
    if i >= 15:
        break
# 10, 11, 12, 13, 14, 15

# Avec step
for i in count(10, 2):
    print(i)
    if i >= 20:
        break
# 10, 12, 14, 16, 18, 20

# Avec floats
list(islice(count(0.5, 0.5), 5))  # [0.5, 1.0, 1.5, 2.0, 2.5]

# cycle(iterable) - Répète indéfiniment
colors = cycle(['red', 'green', 'blue'])
list(islice(colors, 7))
# ['red', 'green', 'blue', 'red', 'green', 'blue', 'red']

# Usage pratique
for i, color in zip(range(5), cycle(['red', 'blue'])):
    print(f"{i}: {color}")

# repeat(object, times=None) - Répète un objet
list(repeat('A', 5))              # ['A', 'A', 'A', 'A', 'A']
list(repeat(10, 3))               # [10, 10, 10]

# Infini si times non spécifié
list(islice(repeat('X'), 4))      # ['X', 'X', 'X', 'X']

# Utile avec map
list(map(pow, range(10), repeat(2)))  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]


[OK] ITÉRATEURS TERMINANTS


# accumulate(iterable, func=operator.add) - Sommes cumulatives
list(accumulate([1, 2, 3, 4, 5]))
# [1, 3, 6, 10, 15]  (sommes cumulatives)

# Avec fonction custom
list(accumulate([1, 2, 3, 4, 5], lambda x, y: x * y))
# [1, 2, 6, 24, 120]  (produits cumulatifs)

# Avec operator
import operator
list(accumulate([1, 2, 3, 4], operator.mul))  # [1, 2, 6, 24]

# Max cumulatif
list(accumulate([5, 2, 9, 1, 7], max))  # [5, 5, 9, 9, 9]

# Avec initial (Python 3.8+)
list(accumulate([1, 2, 3], initial=100))  # [100, 101, 103, 106]

# chain(*iterables) - Concatène plusieurs itérables
list(chain([1, 2], [3, 4], [5, 6]))
# [1, 2, 3, 4, 5, 6]

list(chain('ABC', 'DEF'))
# ['A', 'B', 'C', 'D', 'E', 'F']

# chain.from_iterable(iterable) - À partir d'un itérable d'itérables
list(chain.from_iterable(['ABC', 'DEF']))
# ['A', 'B', 'C', 'D', 'E', 'F']

list(chain.from_iterable([[1, 2], [3, 4], [5]]))
# [1, 2, 3, 4, 5]

# compress(data, selectors) - Filtre avec masque booléen
list(compress('ABCDEF', [1, 0, 1, 0, 1, 1]))
# ['A', 'C', 'E', 'F']

list(compress([1, 2, 3, 4, 5], [True, False, True, False, True]))
# [1, 3, 5]

# dropwhile(predicate, iterable) - Supprime tant que vrai
list(dropwhile(lambda x: x < 5, [1, 4, 6, 4, 1]))
# [6, 4, 1]  (commence à partir du premier False)

list(dropwhile(lambda x: x < 3, [1, 2, 3, 4, 1, 2]))
# [3, 4, 1, 2]

# filterfalse(predicate, iterable) - Inverse de filter()
list(filterfalse(lambda x: x % 2, range(10)))
# [0, 2, 4, 6, 8]  (nombres pairs)

list(filter(lambda x: x % 2, range(10)))
# [1, 3, 5, 7, 9]  (nombres impairs - filter standard)

# groupby(iterable, key=None) - Groupe éléments consécutifs
data = [1, 1, 2, 2, 2, 3, 1, 1]
for key, group in groupby(data):
    print(key, list(group))
# 1 [1, 1]
# 2 [2, 2, 2]
# 3 [3]
# 1 [1, 1]

# Avec key function
names = ['Alice', 'Bob', 'Charlie', 'David', 'Eve']
for key, group in groupby(names, key=len):
    print(f"Longueur {key}: {list(group)}")
# Longueur 5: ['Alice']
# Longueur 3: ['Bob']
# Longueur 7: ['Charlie']
# Longueur 5: ['David']
# Longueur 3: ['Eve']

# IMPORTANT: trier avant groupby!
names = ['Alice', 'Bob', 'Charlie', 'David', 'Eve']
names_sorted = sorted(names, key=len)
for key, group in groupby(names_sorted, key=len):
    print(f"Longueur {key}: {list(group)}")

# islice(iterable, stop) ou islice(iterable, start, stop, step)
list(islice(range(10), 5))           # [0, 1, 2, 3, 4]
list(islice(range(10), 2, 8))        # [2, 3, 4, 5, 6, 7]
list(islice(range(10), 0, 10, 2))    # [0, 2, 4, 6, 8]

# Avec itérateurs infinis
list(islice(count(), 5))             # [0, 1, 2, 3, 4]
list(islice(cycle('AB'), 8))         # ['A', 'B', 'A', 'B', 'A', 'B', 'A', 'B']

# pairwise(iterable) - Paires consécutives (Python 3.10+)
list(pairwise([1, 2, 3, 4, 5]))
# [(1, 2), (2, 3), (3, 4), (4, 5)]

list(pairwise('ABCDEF'))
# [('A', 'B'), ('B', 'C'), ('C', 'D'), ('D', 'E'), ('E', 'F')]

# starmap(function, iterable) - map avec unpacking
list(starmap(pow, [(2, 5), (3, 2), (10, 3)]))
# [32, 9, 1000]  (2^5, 3^2, 10^3)

list(starmap(lambda x, y: x + y, [(1, 2), (3, 4), (5, 6)]))
# [3, 7, 11]

# takewhile(predicate, iterable) - Prend tant que vrai
list(takewhile(lambda x: x < 5, [1, 4, 6, 4, 1]))
# [1, 4]  (s'arrête au premier False)

list(takewhile(lambda x: x < 10, count()))
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# tee(iterable, n=2) - Crée n itérateurs indépendants
it1, it2 = tee(range(5))
list(it1)  # [0, 1, 2, 3, 4]
list(it2)  # [0, 1, 2, 3, 4]

# ATTENTION: ne pas utiliser l'itérable original après tee()

# zip_longest(*iterables, fillvalue=None) - Zip jusqu'au plus long
list(zip_longest('ABCD', 'xy', fillvalue='-'))
# [('A', 'x'), ('B', 'y'), ('C', '-'), ('D', '-')]

# zip standard s'arrête au plus court
list(zip('ABCD', 'xy'))
# [('A', 'x'), ('B', 'y')]


[OK] ITÉRATEURS COMBINATOIRES


# product(*iterables, repeat=1) - Produit cartésien
list(product('AB', '12'))
# [('A', '1'), ('A', '2'), ('B', '1'), ('B', '2')]

list(product([1, 2], [3, 4]))
# [(1, 3), (1, 4), (2, 3), (2, 4)]

# Avec repeat (équivalent à boucles imbriquées)
list(product('AB', repeat=2))
# [('A', 'A'), ('A', 'B'), ('B', 'A'), ('B', 'B')]

list(product(range(2), repeat=3))
# [(0,0,0), (0,0,1), (0,1,0), (0,1,1), (1,0,0), (1,0,1), (1,1,0), (1,1,1)]

# Plusieurs itérables
list(product('AB', '12', 'XY'))
# [('A', '1', 'X'), ('A', '1', 'Y'), ('A', '2', 'X'), ...]

# permutations(iterable, r=None) - Permutations
list(permutations('ABC'))
# [('A','B','C'), ('A','C','B'), ('B','A','C'), ('B','C','A'), ('C','A','B'), ('C','B','A')]

list(permutations('ABC', 2))
# [('A','B'), ('A','C'), ('B','A'), ('B','C'), ('C','A'), ('C','B')]

list(permutations([1, 2, 3], 2))
# [(1,2), (1,3), (2,1), (2,3), (3,1), (3,2)]

# Nombre de permutations: n! / (n-r)!
len(list(permutations('ABC', 2)))  # 6 = 3! / 1!

# combinations(iterable, r) - Combinaisons (ordre n'importe pas)
list(combinations('ABCD', 2))
# [('A','B'), ('A','C'), ('A','D'), ('B','C'), ('B','D'), ('C','D')]

list(combinations([1, 2, 3, 4], 3))
# [(1,2,3), (1,2,4), (1,3,4), (2,3,4)]

# Nombre de combinaisons: n! / (r! * (n-r)!)
len(list(combinations('ABCD', 2)))  # 6 = 4! / (2! * 2!)

# combinations_with_replacement(iterable, r) - Avec répétitions
list(combinations_with_replacement('AB', 2))
# [('A','A'), ('A','B'), ('B','B')]

list(combinations_with_replacement([1, 2, 3], 2))
# [(1,1), (1,2), (1,3), (2,2), (2,3), (3,3)]

# Nombre: (n+r-1)! / (r! * (n-1)!)


[OK] EXEMPLES PRATIQUES


# 1. Sliding window
def sliding_window(iterable, n):
    """Fenêtre glissante de taille n"""
    it = iter(iterable)
    window = list(islice(it, n))
    if len(window) == n:
        yield tuple(window)
    for item in it:
        window.pop(0)
        window.append(item)
        yield tuple(window)

list(sliding_window([1, 2, 3, 4, 5], 3))
# [(1,2,3), (2,3,4), (3,4,5)]

# Version avec collections.deque (plus efficace)
from collections import deque

def sliding_window_fast(iterable, n):
    it = iter(iterable)
    window = deque(islice(it, n), maxlen=n)
    if len(window) == n:
        yield tuple(window)
    for item in it:
        window.append(item)
        yield tuple(window)


# 2. Grouper par chunks
def grouper(iterable, n, fillvalue=None):
    """Grouper en chunks de taille n"""
    args = [iter(iterable)] * n
    return zip_longest(*args, fillvalue=fillvalue)

list(grouper('ABCDEFG', 3, 'x'))
# [('A','B','C'), ('D','E','F'), ('G','x','x')]


# 3. Flatten (aplatir liste)
def flatten(list_of_lists):
    """Aplatir une liste de listes"""
    return chain.from_iterable(list_of_lists)

list(flatten([[1, 2], [3, 4], [5, 6]]))
# [1, 2, 3, 4, 5, 6]


# 4. Unique (garder ordre)
def unique_everseen(iterable, key=None):
    """Éléments uniques préservant l'ordre"""
    seen = set()
    seen_add = seen.add
    if key is None:
        for element in filterfalse(seen.__contains__, iterable):
            seen_add(element)
            yield element
    else:
        for element in iterable:
            k = key(element)
            if k not in seen:
                seen_add(k)
                yield element

list(unique_everseen([1, 2, 3, 2, 1, 4, 5, 4]))
# [1, 2, 3, 4, 5]


# 5. Paires adjacentes différentes
def pairwise_different(iterable):
    """Paires consécutives différentes"""
    a, b = tee(iterable)
    next(b, None)
    return ((x, y) for x, y in zip(a, b) if x != y)

list(pairwise_different([1, 1, 2, 3, 3, 4]))
# [(1,2), (2,3), (3,4)]


# 6. Round-robin (répartition équitable)
def roundrobin(*iterables):
    """Round-robin entre plusieurs itérables"""
    num_active = len(iterables)
    nexts = cycle(iter(it).__next__ for it in iterables)
    while num_active:
        try:
            for next_func in nexts:
                yield next_func()
        except StopIteration:
            num_active -= 1
            nexts = cycle(islice(nexts, num_active))

list(roundrobin('ABC', 'D', 'EF'))
# ['A', 'D', 'E', 'B', 'F', 'C']


# 7. Partitionner selon prédicat
def partition(pred, iterable):
    """Séparer en deux selon prédicat"""
    t1, t2 = tee(iterable)
    return filter(pred, t1), filterfalse(pred, t2)

evens, odds = partition(lambda x: x % 2 == 0, range(10))
list(evens), list(odds)
# ([0, 2, 4, 6, 8], [1, 3, 5, 7, 9])


# 8. Toutes les paires (sans répétition)
def all_pairs(iterable):
    """Toutes les paires uniques"""
    return combinations(iterable, 2)

list(all_pairs([1, 2, 3, 4]))
# [(1,2), (1,3), (1,4), (2,3), (2,4), (3,4)]


# 9. N premiers éléments d'un itérateur infini
def take(n, iterable):
    """Prendre n premiers éléments"""
    return list(islice(iterable, n))

take(5, count(10))  # [10, 11, 12, 13, 14]


# 10. Répéter chaque élément n fois
def repeat_each(iterable, n):
    """Répéter chaque élément n fois"""
    return chain.from_iterable(repeat(item, n) for item in iterable)

list(repeat_each('ABC', 3))
# ['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C']


# 11. Consommer un itérateur
def consume(iterator, n=None):
    """Avancer dans un itérateur de n éléments (ou tout)"""
    if n is None:
        deque(iterator, maxlen=0)
    else:
        next(islice(iterator, n, n), None)


# 12. Nth élément
def nth(iterable, n, default=None):
    """Retourner le n-ième élément"""
    return next(islice(iterable, n, None), default)

nth('ABCDEF', 3)  # 'D'


# 13. Quantify (compter selon prédicat)
def quantify(iterable, pred=bool):
    """Compter éléments satisfaisant prédicat"""
    return sum(map(pred, iterable))

quantify([1, 2, 3, 4, 5], lambda x: x % 2 == 0)  # 2


# 14. Dotproduct (produit scalaire)
def dotproduct(vec1, vec2):
    """Produit scalaire de deux vecteurs"""
    return sum(starmap(operator.mul, zip(vec1, vec2)))

dotproduct([1, 2, 3], [4, 5, 6])  # 32


# 15. Permutations avec répétitions
def product_repeat(iterable, r):
    """Permutations avec répétitions"""
    return product(iterable, repeat=r)

list(product_repeat('AB', 3))
# [('A','A','A'), ('A','A','B'), ('A','B','A'), ...]


[OK] PATTERNS COURANTS


# Pattern 1: Traiter par chunks
def process_in_chunks(data, chunk_size):
    """Traiter données par chunks"""
    for chunk in grouper(data, chunk_size):
        # Traiter chunk
        yield process(chunk)


# Pattern 2: Pipeline de transformations
def pipeline(data):
    """Pipeline de transformations"""
    data = map(str.strip, data)           # Nettoyer
    data = filter(bool, data)              # Supprimer vides
    data = map(str.upper, data)            # Majuscules
    return list(data)


# Pattern 3: Combiner plusieurs sources
def merge_sorted(*iterables):
    """Fusionner plusieurs itérables triés"""
    return heapq.merge(*iterables)


# Pattern 4: Cartesian product pour tests
def generate_test_cases():
    """Générer cas de test"""
    inputs = product(
        [True, False],           # bool_flag
        range(5),                # int_value
        ['a', 'b', 'c']          # str_choice
    )
    for flag, value, choice in inputs:
        yield test_case(flag, value, choice)


# Pattern 5: Groupby pour agrégations
def aggregate_by_key(data):
    """Agréger par clé"""
    data = sorted(data, key=lambda x: x['key'])
    for key, group in groupby(data, key=lambda x: x['key']):
        items = list(group)
        yield {
            'key': key,
            'count': len(items),
            'sum': sum(item['value'] for item in items)
        }


[OK] PERFORMANCE & OPTIMISATION


# itertools est TRÈS efficace en mémoire (lazy evaluation)

# [X] Mauvais: crée toutes les combinaisons en mémoire
all_combos = list(product(range(1000), repeat=3))  # 1 milliard!

# [OK] Bon: traite une à une
for combo in product(range(1000), repeat=3):
    if satisfies_condition(combo):
        process(combo)
        break  # Arrêt dès qu'on trouve

# Comparer performances
import time

# List comprehension (tout en mémoire)
start = time.time()
data = [x * 2 for x in range(1000000)]
print(f"List: {time.time() - start:.4f}s")

# Generator (lazy)
start = time.time()
data = (x * 2 for x in range(1000000))
print(f"Generator: {time.time() - start:.4f}s")  # Quasi instantané!

# Consommation
start = time.time()
for item in data:
    pass
print(f"Consumption: {time.time() - start:.4f}s")


# Éviter de convertir en list inutilement
# [X] Mauvais
total = sum(list(map(lambda x: x * 2, range(1000))))

# [OK] Bon
total = sum(map(lambda x: x * 2, range(1000)))


[OK] COMBINAISONS PUISSANTES


# 1. Tous les subsets (powerset)
def powerset(iterable):
    """Tous les sous-ensembles"""
    s = list(iterable)
    return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))

list(powerset([1, 2, 3]))
# [(), (1,), (2,), (3,), (1,2), (1,3), (2,3), (1,2,3)]


# 2. Permutations uniques
def unique_permutations(iterable, r=None):
    """Permutations sans doublons"""
    return set(permutations(iterable, r))

list(unique_permutations('AAB', 2))
# {('A','A'), ('A','B'), ('B','A')}


# 3. Matrice transpose
def transpose(matrix):
    """Transposer une matrice"""
    return list(zip(*matrix))

transpose([[1, 2, 3], [4, 5, 6]])
# [(1, 4), (2, 5), (3, 6)]


# 4. Running average
def running_average(iterable):
    """Moyenne glissante"""
    total = 0
    count = 0
    for value in iterable:
        total += value
        count += 1
        yield total / count

list(running_average([10, 20, 30, 40]))
# [10.0, 15.0, 20.0, 25.0]


# 5. Fenêtre avec padding
def windowed(iterable, n, fillvalue=None):
    """Fenêtre avec padding début/fin"""
    it = chain([fillvalue] * (n-1), iterable, [fillvalue] * (n-1))
    return sliding_window(it, n)


# 6. Interleave (entrelacer)
def interleave(*iterables):
    """Entrelacer plusieurs itérables"""
    return chain.from_iterable(zip(*iterables))

list(interleave([1, 2, 3], ['a', 'b', 'c']))
# [1, 'a', 2, 'b', 3, 'c']


# 7. Grouper par condition
def group_by_condition(iterable, condition):
    """Grouper éléments consécutifs selon condition"""
    for key, group in groupby(iterable, key=condition):
        yield key, list(group)

list(group_by_condition([1, -2, -3, 4, 5, -6], lambda x: x > 0))
# [(True, [1]), (False, [-2, -3]), (True, [4, 5]), (False, [-6])]


[OK] AVEC AUTRES MODULES


# Avec operator
import operator

# Somme cumulatives avec operator.add
list(accumulate([1, 2, 3, 4], operator.add))  # [1, 3, 6, 10]

# Produit avec operator.mul
list(accumulate([1, 2, 3, 4], operator.mul))  # [1, 2, 6, 24]

# Avec functools
from functools import reduce

# reduce vs accumulate
reduce(operator.add, [1, 2, 3, 4])           # 10 (résultat final)
list(accumulate([1, 2, 3, 4], operator.add)) # [1,3,6,10] (tous)

# Avec collections
from collections import Counter, defaultdict

# Compter avec groupby
def count_with_groupby(iterable):
    return {k: sum(1 for _ in g) for k, g in groupby(sorted(iterable))}

count_with_groupby([1, 2, 2, 3, 3, 3])
# {1: 1, 2: 2, 3: 3}

# Avec heapq (merge sorted)
import heapq

list(heapq.merge([1, 3, 5], [2, 4, 6]))
# [1, 2, 3, 4, 5, 6]


[OK] ERREURS COURANTES


# 1. Consommer un itérateur plusieurs fois
it = filter(lambda x: x > 5, range(10))
list(it)  # [6, 7, 8, 9]
list(it)  # [] - Vide! Itérateur épuisé

# Solution: tee() ou convertir en list
it = filter(lambda x: x > 5, range(10))
it1, it2 = tee(it)


# 2. Modifier l'original après tee()
original = iter([1, 2, 3, 4, 5])
it1, it2 = tee(original)
next(original)  # [X] Ne pas faire!


# 3. Oublier de trier avant groupby
data = [1, 2, 1, 2, 3]
# [X] Mauvais
list(groupby(data))  # Groupes incorrects

# [OK] Bon
list(groupby(sorted(data)))


# 4. Permutations vs combinations
permutations('AB', 2)  # [('A','B'), ('B','A')] - ordre compte
combinations('AB', 2)   # [('A','B')] - ordre ne compte pas


# 5. islice avec step négatif
# islice(range(10), 9, 0, -1)  # [X] Ne fonctionne pas!
# Solution: convertir en list et slicer
list(range(10))[9::-1]


# 6. Product avec grands itérables
# [X] Mauvais (mémoire/temps)
list(product(range(1000), repeat=5))  # 1 trillion d'éléments!

# [OK] Bon (lazy)
for item in product(range(1000), repeat=5):
    if condition(item):
        break


[OK] TESTS & DEBUGGING


# Afficher itérateur sans le consommer
def peek(iterable, n=5):
    """Afficher n premiers éléments"""
    it1, it2 = tee(iterable)
    print(list(islice(it1, n)))
    return it2

# Vérifier qu'itérateur produit attendu
def verify_iterator(iterator, expected):
    """Vérifier sortie itérateur"""
    result = list(iterator)
    assert result == expected, f"Got {result}, expected {expected}"

# Compter éléments sans consommer
def count_iterator(iterable):
    """Compter éléments"""
    return sum(1 for _ in iterable)


[OK] RECETTES ITERTOOLS (DOCUMENTATION)


# Ces recettes sont dans la documentation officielle
# https://docs.python.org/3/library/itertools.html#itertools-recipes

def tail(n, iterable):
    """Retourner les n derniers éléments"""
    return iter(deque(iterable, maxlen=n))

def prepend(value, iterable):
    """Ajouter au début"""
    return chain([value], iterable)

def tabulate(function, start=0):
    """Retourner function(0), function(1), ..."""
    return map(function, count(start))

def repeatfunc(func, times=None, *args):
    """Répéter appel à func"""
    if times is None:
        return starmap(func, repeat(args))
    return starmap(func, repeat(args, times))

def ncycles(iterable, n):
    """Répéter itérable n fois"""
    return chain.from_iterable(repeat(tuple(iterable), n))


[OK] BONNES PRATIQUES


# [OK] Utiliser itertools pour efficacité mémoire
# [OK] Préférer itertools aux list comprehensions pour grands datasets
# [OK] Trier avant groupby
# [OK] Utiliser tee() pour dupliquer itérateurs
# [OK] Combiner avec operator pour fonctions courantes
# [OK] Utiliser islice pour limiter itérateurs infinis
# [OK] chain.from_iterable pour aplatir
# [OK] starmap pour fonctions à plusieurs arguments
# [OK] compress pour filtrage avec masque booléen

# [X] Ne pas convertir en list inutilement
# [X] Ne pas consommer itérateur plusieurs fois
# [X] Ne pas modifier original après tee()
# [X] Ne pas oublier que groupby groupe consécutifs seulement
# [X] Attention aux itérateurs infinis sans limite


[OK] CAS D'USAGE AVANCÉS


# 1. Générer mots de passe
def generate_passwords(length, charset):
    """Générer tous les mots de passe possibles"""
    return (''.join(p) for p in product(charset, repeat=length))

# Mots de passe de 3 caractères
passwords = generate_passwords(3, 'abc')
list(islice(passwords, 10))
# ['aaa', 'aab', 'aac', 'aba', 'abb', 'abc', 'aca', 'acb', 'acc', 'baa']


# 2. Simulation Monte Carlo
from random import random

def monte_carlo_pi(n):
    """Estimer π avec Monte Carlo"""
    inside = sum(1 for _ in range(n) 
                 if random()**2 + random()**2 <= 1)
    return 4 * inside / n

# Avec itertools
def monte_carlo_pi_iter(n):
    points = ((random(), random()) for _ in repeat(None, n))
    inside = sum(1 for x, y in points if x*x + y*y <= 1)
    return 4 * inside / n


# 3. Générateur de Fibonacci optimisé
def fibonacci_sequence():
    """Séquence de Fibonacci infinie"""
    a, b = 0, 1
    yield a
    yield b
    for _ in count():
        a, b = b, a + b
        yield b

list(islice(fibonacci_sequence(), 10))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]


# 4. Traiter fichier CSV par chunks
def process_csv_chunks(filename, chunk_size=1000):
    """Traiter CSV par chunks"""
    with open(filename) as f:
        lines = (line.strip() for line in f)
        headers = next(lines)
        
        for chunk in grouper(lines, chunk_size):
            chunk = list(filter(None, chunk))  # Supprimer None du padding
            yield process_chunk(chunk)


# 5. Cache LRU manuel avec itertools
class LRUCache:
    def __init__(self, size):
        self.size = size
        self.cache = {}
        self.access_count = count()
    
    def get(self, key):
        if key in self.cache:
            value, _ = self.cache[key]
            self.cache[key] = (value, next(self.access_count))
            return value
        return None
    
    def set(self, key, value):
        if len(self.cache) >= self.size:
            # Supprimer le moins récemment utilisé
            lru_key = min(self.cache, key=lambda k: self.cache[k][1])
            del self.cache[lru_key]
        self.cache[key] = (value, next(self.access_count))


# 6. Rotation de liste (circular shift)
def rotate(iterable, n):
    """Rotation circulaire de n positions"""
    deq = deque(iterable)
    deq.rotate(n)
    return list(deq)

# Avec itertools seulement
def rotate_iter(iterable, n):
    items = list(iterable)
    n = n % len(items) if items else 0
    return chain(islice(items, n, None), islice(items, n))

list(rotate_iter([1, 2, 3, 4, 5], 2))
# [3, 4, 5, 1, 2]


# 7. Batch processing avec timeout
import time

def batch_with_timeout(iterable, batch_size, timeout):
    """Traiter par batch avec timeout"""
    batch = []
    start_time = time.time()
    
    for item in iterable:
        batch.append(item)
        
        if len(batch) >= batch_size or time.time() - start_time >= timeout:
            yield batch
            batch = []
            start_time = time.time()
    
    if batch:
        yield batch


# 8. Parallèle avec itertools + multiprocessing
from multiprocessing import Pool

def parallel_process(iterable, func, chunk_size=100):
    """Traiter en parallèle par chunks"""
    with Pool() as pool:
        chunks = grouper(iterable, chunk_size)
        for result in pool.imap(lambda chunk: list(map(func, chunk)), chunks):
            yield from result


# 9. Zipper avec alignement custom
def zip_align(iterable1, iterable2, align='left'):
    """Zipper avec alignement"""
    if align == 'left':
        return zip(iterable1, iterable2)
    elif align == 'right':
        return zip_longest(iterable1, iterable2)
    elif align == 'center':
        len1, len2 = len(list(iterable1)), len(list(iterable2))
        diff = abs(len1 - len2) // 2
        if len1 > len2:
            iterable2 = chain(repeat(None, diff), iterable2)
        else:
            iterable1 = chain(repeat(None, diff), iterable1)
        return zip_longest(iterable1, iterable2)


# 10. Stream processing avec buffer
def buffered_stream(iterable, buffer_size):
    """Stream avec buffer"""
    buffer = []
    for item in iterable:
        buffer.append(item)
        if len(buffer) >= buffer_size:
            yield list(buffer)
            buffer = []
    if buffer:
        yield buffer


[OK] PATTERNS FONCTIONNELS


# 1. Map-Reduce avec itertools
def map_reduce(iterable, mapper, reducer):
    """Pattern Map-Reduce"""
    mapped = map(mapper, iterable)
    grouped = groupby(sorted(mapped, key=lambda x: x[0]), key=lambda x: x[0])
    return [(key, reducer(values)) for key, values in 
            ((k, (v for _, v in g)) for k, g in grouped)]

# Exemple: compter mots
def word_count(text):
    words = text.lower().split()
    return map_reduce(
        words,
        lambda w: (w, 1),
        sum
    )


# 2. Lazy evaluation pipeline
class LazyPipeline:
    def __init__(self, iterable):
        self.iterable = iterable
    
    def map(self, func):
        self.iterable = map(func, self.iterable)
        return self
    
    def filter(self, pred):
        self.iterable = filter(pred, self.iterable)
        return self
    
    def take(self, n):
        self.iterable = islice(self.iterable, n)
        return self
    
    def collect(self):
        return list(self.iterable)

# Usage
result = (LazyPipeline(range(100))
    .filter(lambda x: x % 2 == 0)
    .map(lambda x: x ** 2)
    .take(5)
    .collect())
# [0, 4, 16, 36, 64]


# 3. Memoization avec itertools
def memoize_iter(func):
    """Memoization pour générateurs"""
    cache = {}
    def wrapper(*args):
        if args not in cache:
            cache[args] = list(func(*args))
        return iter(cache[args])
    return wrapper

@memoize_iter
def expensive_generator(n):
    for i in range(n):
        # Calcul coûteux
        yield i ** 2


# 4. Currying avec partial et itertools
from functools import partial

def curry_with_iter(func):
    """Curry automatique"""
    def curried(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except TypeError:
            return partial(curried, *args, **kwargs)
    return curried


# 5. Composeur de fonctions
def compose(*functions):
    """Composer plusieurs fonctions"""
    def inner(arg):
        return reduce(lambda x, f: f(x), reversed(functions), arg)
    return inner

# Usage avec itertools
pipeline = compose(
    lambda x: map(lambda n: n * 2, x),
    lambda x: filter(lambda n: n > 10, x),
    lambda x: list(x)
)
pipeline([1, 5, 10, 15])  # [20, 30]


[OK] ALGORITHMES CLASSIQUES


# 1. Recherche binaire (avec bisect + itertools)
import bisect

def binary_search_all(sorted_list, value):
    """Trouver toutes les occurrences"""
    left = bisect.bisect_left(sorted_list, value)
    right = bisect.bisect_right(sorted_list, value)
    return list(islice(sorted_list, left, right))


# 2. Merge k sorted lists
import heapq

def merge_k_sorted(lists):
    """Fusionner k listes triées"""
    return list(heapq.merge(*lists))


# 3. Génération de sous-séquences
def all_subsequences(iterable):
    """Toutes les sous-séquences (préserve ordre)"""
    items = list(iterable)
    return chain.from_iterable(
        combinations(items, r) for r in range(len(items) + 1)
    )

list(all_subsequences([1, 2, 3]))
# [(), (1,), (2,), (3,), (1,2), (1,3), (2,3), (1,2,3)]


# 4. Longest common subsequence (avec itertools)
def lcs_length(s1, s2):
    """Longueur de la plus longue sous-séquence commune"""
    max_length = 0
    for length in range(1, min(len(s1), len(s2)) + 1):
        for subseq in combinations(s1, length):
            # Vérifier si dans s2 en préservant ordre
            if all(c in s2 for c in subseq):
                max_length = length
    return max_length


# 5. Générer partitions d'un ensemble
def partitions(iterable):
    """Toutes les partitions d'un ensemble"""
    items = list(iterable)
    for n in range(1, len(items) + 1):
        for combo in combinations(items, n):
            remaining = [x for x in items if x not in combo]
            if remaining:
                for part in partitions(remaining):
                    yield [list(combo)] + part
            else:
                yield [list(combo)]


# 6. Cartesian join (SQL-like)
def cartesian_join(left, right, left_key, right_key):
    """Join cartésien avec clés"""
    for l_row, r_row in product(left, right):
        if left_key(l_row) == right_key(r_row):
            yield {**l_row, **r_row}


# 7. Run-length encoding
def run_length_encode(iterable):
    """Encodage par longueur de séquence"""
    return [(key, sum(1 for _ in group)) 
            for key, group in groupby(iterable)]

run_length_encode('aaabbbcc')
# [('a', 3), ('b', 3), ('c', 2)]


# 8. Run-length decoding
def run_length_decode(encoded):
    """Décodage par longueur de séquence"""
    return chain.from_iterable(repeat(char, count) 
                               for char, count in encoded)

list(run_length_decode([('a', 3), ('b', 2)]))
# ['a', 'a', 'a', 'b', 'b']


[OK] DATA SCIENCE & ANALYSE


# 1. Moving average
def moving_average(iterable, n):
    """Moyenne mobile sur n éléments"""
    it = iter(iterable)
    window = deque(islice(it, n), maxlen=n)
    
    if len(window) == n:
        yield sum(window) / n
    
    for item in it:
        window.append(item)
        yield sum(window) / n

list(moving_average([1, 2, 3, 4, 5, 6], 3))
# [2.0, 3.0, 4.0, 5.0]


# 2. Exponential moving average
def exponential_moving_average(iterable, alpha=0.3):
    """EMA avec facteur de lissage alpha"""
    it = iter(iterable)
    ema = next(it)
    yield ema
    
    for value in it:
        ema = alpha * value + (1 - alpha) * ema
        yield ema


# 3. Percentiles avec accumulate
def running_percentile(iterable, percentile):
    """Percentile glissant"""
    buffer = []
    for value in iterable:
        buffer.append(value)
        sorted_buffer = sorted(buffer)
        idx = int(len(sorted_buffer) * percentile / 100)
        yield sorted_buffer[idx]


# 4. Correlation entre deux séries
def correlation(seq1, seq2):
    """Corrélation entre deux séquences"""
    pairs = list(zip(seq1, seq2))
    n = len(pairs)
    
    sum_x = sum(x for x, y in pairs)
    sum_y = sum(y for x, y in pairs)
    sum_xy = sum(x * y for x, y in pairs)
    sum_x2 = sum(x * x for x, y in pairs)
    sum_y2 = sum(y * y for x, y in pairs)
    
    numerator = n * sum_xy - sum_x * sum_y
    denominator = ((n * sum_x2 - sum_x**2) * (n * sum_y2 - sum_y**2)) ** 0.5
    
    return numerator / denominator if denominator != 0 else 0


# 5. Binning/Histogramme
def create_bins(iterable, num_bins):
    """Créer histogramme"""
    values = list(iterable)
    min_val, max_val = min(values), max(values)
    bin_width = (max_val - min_val) / num_bins
    
    bins = [0] * num_bins
    for value in values:
        bin_idx = min(int((value - min_val) / bin_width), num_bins - 1)
        bins[bin_idx] += 1
    
    return bins


# 6. Sampling sans replacement
from random import sample

def reservoir_sampling(iterable, k):
    """Échantillonnage réservoir (pour streams)"""
    reservoir = list(islice(iterable, k))
    
    for i, item in enumerate(iterable, start=k):
        j = random.randint(0, i)
        if j < k:
            reservoir[j] = item
    
    return reservoir


[OK] TRAITEMENT DE TEXTE


# 1. N-grams
def ngrams(iterable, n):
    """Générer n-grams"""
    return zip(*[islice(it, i, None) for i, it in 
                 enumerate(tee(iterable, n))])

list(ngrams('hello', 2))
# [('h', 'e'), ('e', 'l'), ('l', 'l'), ('l', 'o')]

list(ngrams('hello world'.split(), 2))
# [('hello', 'world')]


# 2. Tokenizer simple
def tokenize(text):
    """Tokenizer basique"""
    return chain.from_iterable(
        word.split() for word in text.split()
    )


# 3. Bag of words
def bag_of_words(documents):
    """Créer bag of words"""
    all_words = chain.from_iterable(doc.lower().split() 
                                    for doc in documents)
    word_counts = Counter(all_words)
    return word_counts


# 4. Skip-grams
def skipgrams(sequence, n, k):
    """N-grams avec sauts de k éléments max"""
    for combo in combinations(range(len(sequence)), n):
        if max(combo[i+1] - combo[i] for i in range(n-1)) <= k + 1:
            yield tuple(sequence[i] for i in combo)

list(skipgrams('hello', 2, 1))
# [('h', 'e'), ('h', 'l'), ('e', 'l'), ('e', 'l'), ('l', 'l'), ('l', 'o')]


[OK] GRAPHES & ARBRES


# 1. BFS avec itertools
from collections import deque

def bfs_itertools(graph, start):
    """BFS avec itertools"""
    visited = {start}
    queue = deque([start])
    
    while queue:
        node = queue.popleft()
        yield node
        
        for neighbor in graph.get(node, []):
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)


# 2. Générer tous les chemins
def all_paths(graph, start, end, path=[]):
    """Tous les chemins entre start et end"""
    path = path + [start]
    
    if start == end:
        yield path
    
    for node in graph.get(start, []):
        if node not in path:
            yield from all_paths(graph, node, end, path)


# 3. Traversée d'arbre en profondeur
def dfs_itertools(tree, node='root'):
    """DFS avec itertools"""
    yield node
    for child in tree.get(node, []):
        yield from dfs_itertools(tree, child)


[OK] COMPRESSION & OPTIMISATION


# 1. Difference encoding
def difference_encode(iterable):
    """Encodage par différences"""
    it = iter(iterable)
    prev = next(it)
    yield prev
    
    for current in it:
        yield current - prev
        prev = current

list(difference_encode([10, 15, 17, 20, 25]))
# [10, 5, 2, 3, 5]


# 2. Delta encoding avec accumulate
def delta_decode(encoded):
    """Décodage delta"""
    return list(accumulate(encoded))

delta_decode([10, 5, 2, 3, 5])
# [10, 15, 17, 20, 25]


# 3. Compresser séquences répétitives
def compress_repeats(iterable):
    """Compresser répétitions"""
    for item, group in groupby(iterable):
        count = sum(1 for _ in group)
        if count > 1:
            yield (item, count)
        else:
            yield item


[OK] RESSOURCES & DOCUMENTATION


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

# Recettes itertools (très utile!):
# https://docs.python.org/3/library/itertools.html#itertools-recipes

# More-itertools (extension tierce):
# pip install more-itertools
# https://more-itertools.readthedocs.io/

# Exemples more-itertools:
from more_itertools import *

# chunked() - comme grouper mais plus simple
list(chunked([1, 2, 3, 4, 5], 2))  # [[1, 2], [3, 4], [5]]

# windowed() - fenêtre glissante
list(windowed([1, 2, 3, 4], 3))  # [(1,2,3), (2,3,4)]

# flatten() - aplatir récursivement
list(flatten([[1, [2, 3]], [4, [5]]]))  # [1, 2, 3, 4, 5]

# distinct_permutations() - permutations uniques
list(distinct_permutations('AAB'))  # [('A','A','B'), ('A','B','A'), ('B','A','A')]


[OK] COMPARAISON AVEC ALTERNATIVES


# List comprehension vs itertools
# List comprehension: tout en mémoire
squares = [x**2 for x in range(1000000)]

# itertools: lazy evaluation
squares = map(lambda x: x**2, range(1000000))

# NumPy: vectorisé (le plus rapide pour calculs numériques)
import numpy as np
squares = np.arange(1000000) ** 2

# pandas: pour données structurées
import pandas as pd
df = pd.DataFrame({'x': range(1000)})
df['squares'] = df['x'] ** 2


[OK] AIDE-MÉMOIRE FINAL


# INFINIS
count(start, step)           # 0, 1, 2, 3, ...
cycle(iterable)              # A, B, C, A, B, C, ...
repeat(obj, times)           # obj, obj, obj, ...

# TERMINANTS
accumulate(it, func)         # Sommes/produits cumulatifs
chain(*its)                  # Concaténer itérables
compress(data, selectors)    # Filtrer avec masque booléen
dropwhile(pred, it)          # Supprimer tant que pred vrai
filterfalse(pred, it)        # Inverse de filter
groupby(it, key)             # Grouper consécutifs (TRIER AVANT!)
islice(it, start, stop, step) # Slice d'itérateur
pairwise(it)                 # Paires consécutives (3.10+)
starmap(func, it)            # map avec unpacking
takewhile(pred, it)          # Prendre tant que pred vrai
tee(it, n)                   # Dupliquer itérateur
zip_longest(*its, fillvalue) # Zip au plus long

# COMBINATOIRES
product(*its, repeat)        # Produit cartésien
permutations(it, r)          # Permutations (ordre compte)
combinations(it, r)          # Combinaisons (ordre ne compte pas)
combinations_with_replacement(it, r)  # Avec répétitions

# PATTERNS
# Fenêtre: pairwise() ou sliding_window()
# Chunks: grouper() avec zip_longest
# Flatten: chain.from_iterable()
# Unique: filterfalse() + set
# Pipeline: map -> filter -> map -> ...


[OK] CONCLUSION

# itertools est ESSENTIEL pour:
# [OK] Traitement de grands datasets (lazy evaluation)
# [OK] Génération combinatoire
# [OK] Traitement de streams
# [OK] Programmation fonctionnelle
# [OK] Optimisation mémoire
# [OK] Code élégant et lisible

# Toujours préférer itertools à des boucles manuelles
# pour les opérations courantes!