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


[OK] INTRODUCTION

# functools fournit des fonctions d'ordre supérieur qui agissent sur ou retournent d'autres fonctions
# Module standard Python pour la programmation fonctionnelle

import functools


[OK] @lru_cache - CACHE LRU (Least Recently Used)


# Cache basique (128 entrées max par défaut)
from functools import lru_cache

@lru_cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# Cache avec taille spécifique
@lru_cache(maxsize=256)
def expensive_function(x, y):
    return x ** y

# Cache illimité ([ATTENTION] attention mémoire)
@lru_cache(maxsize=None)
def compute(x):
    return x * 2

# Cache avec typage (ignore les types différents)
@lru_cache(typed=True)
def add(a, b):
    return a + b

add(1, 2)      # Cache séparé
add(1.0, 2.0)  # Cache séparé avec typed=True

# Infos sur le cache
print(fibonacci.cache_info())
# CacheInfo(hits=8, misses=10, maxsize=128, currsize=10)

# Vider le cache
fibonacci.cache_clear()

# Créer fonction non-cachée
uncached_fib = fibonacci.__wrapped__


[OK] @cache - CACHE ILLIMITÉ (Python 3.9+)


from functools import cache

# Équivalent à @lru_cache(maxsize=None)
@cache
def factorial(n):
    return n * factorial(n-1) if n else 1

# Plus simple que lru_cache pour cache illimité
# Pas de maxsize, pas de typed
# Plus rapide et moins de mémoire overhead


[OK] partial - APPLICATION PARTIELLE


from functools import partial

# Créer fonction avec args pré-remplis
def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)

print(square(5))  # 25
print(cube(5))    # 125

# Avec args positionnels
def greet(greeting, name):
    return f"{greeting}, {name}!"

say_hello = partial(greet, "Hello")
print(say_hello("Alice"))  # Hello, Alice!

# Avec args multiples
def multiply(a, b, c):
    return a * b * c

double_and = partial(multiply, 2)
print(double_and(3, 4))  # 24

# Callback pour GUI/API
from functools import partial

def button_clicked(button_id, event):
    print(f"Button {button_id} clicked")

# Créer callbacks spécifiques
button1_callback = partial(button_clicked, "btn1")
button2_callback = partial(button_clicked, "btn2")

# Utile avec map/filter
numbers = [1, 2, 3, 4, 5]
multiply_by_10 = partial(lambda x, m: x * m, m=10)
result = list(map(multiply_by_10, numbers))


[OK] partialmethod - PARTIAL POUR MÉTHODES


from functools import partialmethod

class Cell:
    def __init__(self):
        self._alive = False
    
    def set_state(self, state):
        self._alive = state
    
    # Créer méthodes spécialisées
    set_alive = partialmethod(set_state, True)
    set_dead = partialmethod(set_state, False)

cell = Cell()
cell.set_alive()  # Équivalent à cell.set_state(True)


[OK] reduce - RÉDUCTION


from functools import reduce

# Somme
numbers = [1, 2, 3, 4, 5]
sum_result = reduce(lambda x, y: x + y, numbers)
print(sum_result)  # 15

# Avec valeur initiale
sum_with_init = reduce(lambda x, y: x + y, numbers, 10)
print(sum_with_init)  # 25

# Produit
product = reduce(lambda x, y: x * y, numbers)
print(product)  # 120

# Maximum
maximum = reduce(lambda x, y: x if x > y else y, numbers)
print(maximum)  # 5

# Concaténation
words = ['Hello', ' ', 'World', '!']
sentence = reduce(lambda x, y: x + y, words)
print(sentence)  # Hello World!

# Flatten liste
nested = [[1, 2], [3, 4], [5, 6]]
flat = reduce(lambda x, y: x + y, nested)
print(flat)  # [1, 2, 3, 4, 5, 6]

# Compter occurrences
from collections import defaultdict
items = ['a', 'b', 'a', 'c', 'b', 'a']
counts = reduce(
    lambda acc, x: {**acc, x: acc.get(x, 0) + 1},
    items,
    {}
)

# Alternative plus lisible avec fonction nommée
def accumulate(acc, item):
    acc[item] = acc.get(item, 0) + 1
    return acc

counts = reduce(accumulate, items, {})


[OK] @wraps - PRÉSERVER MÉTADONNÉES


from functools import wraps

# Sans @wraps (perd métadonnées)
def decorator_bad(func):
    def wrapper(*args, **kwargs):
        """Wrapper docstring"""
        return func(*args, **kwargs)
    return wrapper

# Avec @wraps (préserve métadonnées)
def decorator_good(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """Wrapper docstring"""
        return func(*args, **kwargs)
    return wrapper

@decorator_good
def example():
    """Example docstring"""
    pass

print(example.__name__)  # 'example' au lieu de 'wrapper'
print(example.__doc__)   # 'Example docstring'

# Décorateur avec arguments
def repeat(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    """Greet someone"""
    print(f"Hello, {name}")

# Décorateur de timing
import time

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end-start:.4f}s")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)


[OK] update_wrapper - VERSION FONCTION DE @wraps


from functools import update_wrapper

def decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    
    # Équivalent à @wraps(func)
    update_wrapper(wrapper, func)
    return wrapper

# Avec attributs personnalisés
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__',
                       '__annotations__', '__doc__')
WRAPPER_UPDATES = ('__dict__',)

def decorator_custom(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    
    update_wrapper(
        wrapper, func,
        assigned=WRAPPER_ASSIGNMENTS,
        updated=WRAPPER_UPDATES
    )
    return wrapper


[OK] @singledispatch - DISPATCH PAR TYPE


from functools import singledispatch

# Fonction générique
@singledispatch
def process(arg):
    print(f"Processing {type(arg)}: {arg}")

# Spécialisation pour int
@process.register
def _(arg: int):
    print(f"Processing integer: {arg * 2}")

# Spécialisation pour str
@process.register
def _(arg: str):
    print(f"Processing string: {arg.upper()}")

# Spécialisation pour list
@process.register(list)
def _(arg):
    print(f"Processing list of {len(arg)} items")

process(42)        # Processing integer: 84
process("hello")   # Processing string: HELLO
process([1, 2])    # Processing list of 2 items
process(3.14)      # Processing <class 'float'>: 3.14

# Exemple pratique: formater différents types
@singledispatch
def format_value(val):
    return str(val)

@format_value.register(int)
def _(val):
    return f"{val:,}"

@format_value.register(float)
def _(val):
    return f"{val:.2f}"

@format_value.register(list)
def _(val):
    return f"[{', '.join(map(format_value, val))}]"

print(format_value(1000000))      # 1,000,000
print(format_value(3.14159))      # 3.14
print(format_value([1, 2.5, 3]))  # [1, 2.50, 3]

# Vérifier les implémentations
print(process.registry.keys())  # Types enregistrés


[OK] @singledispatchmethod - DISPATCH POUR MÉTHODES (3.8+)


from functools import singledispatchmethod

class Formatter:
    @singledispatchmethod
    def format(self, arg):
        return f"Generic: {arg}"
    
    @format.register
    def _(self, arg: int):
        return f"Integer: {arg:,}"
    
    @format.register
    def _(self, arg: str):
        return f"String: '{arg}'"
    
    @format.register(list)
    def _(self, arg):
        return f"List: [{len(arg)} items]"

fmt = Formatter()
print(fmt.format(1000))      # Integer: 1,000
print(fmt.format("test"))    # String: 'test'
print(fmt.format([1, 2, 3])) # List: [3 items]


[OK] @total_ordering - COMPARAISONS COMPLÈTES


from functools import total_ordering

# Définir seulement __eq__ et un autre (ex: __lt__)
# total_ordering génère le reste automatiquement
@total_ordering
class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
    
    def __eq__(self, other):
        return self.grade == other.grade
    
    def __lt__(self, other):
        return self.grade < other.grade
    
    # __le__, __gt__, __ge__ générés automatiquement

alice = Student("Alice", 85)
bob = Student("Bob", 90)

print(alice < bob)   # True
print(alice <= bob)  # True (généré)
print(alice > bob)   # False (généré)
print(alice >= bob)  # False (généré)
print(alice == bob)  # False

# Sans total_ordering, il faudrait définir:
# __eq__, __ne__, __lt__, __le__, __gt__, __ge__

# Exemple avec classe Version
@total_ordering
class Version:
    def __init__(self, version_string):
        self.parts = tuple(map(int, version_string.split('.')))
    
    def __eq__(self, other):
        return self.parts == other.parts
    
    def __lt__(self, other):
        return self.parts < other.parts
    
    def __repr__(self):
        return '.'.join(map(str, self.parts))

v1 = Version("1.2.3")
v2 = Version("1.2.10")
v3 = Version("2.0.0")

print(sorted([v3, v1, v2]))  # [1.2.3, 1.2.10, 2.0.0]


[OK] cached_property - PROPRIÉTÉ CACHÉE (3.8+)


from functools import cached_property

class DataProcessor:
    def __init__(self, data):
        self.data = data
    
    @cached_property
    def processed_data(self):
        """Calculé une seule fois, puis caché"""
        print("Processing data...")
        return [x * 2 for x in self.data]
    
    @cached_property
    def summary(self):
        print("Computing summary...")
        return {
            'count': len(self.data),
            'sum': sum(self.data),
            'avg': sum(self.data) / len(self.data)
        }

processor = DataProcessor([1, 2, 3, 4, 5])
print(processor.processed_data)  # Processing data...
print(processor.processed_data)  # Pas de calcul (caché)
print(processor.summary)         # Computing summary...
print(processor.summary)         # Pas de calcul (caché)

# Différence avec @property (toujours recalculé)
class Example:
    @property
    def always_computed(self):
        print("Computing...")
        return 42
    
    @cached_property
    def computed_once(self):
        print("Computing once...")
        return 42


[OK] cmp_to_key - CONVERTIR FONCTION COMPARAISON


from functools import cmp_to_key

# Ancienne fonction de comparaison (-1, 0, 1)
def compare_length(x, y):
    if len(x) < len(y):
        return -1
    elif len(x) > len(y):
        return 1
    return 0

words = ["python", "is", "awesome", "!"]

# Convertir pour sorted() qui attend une key function
sorted_words = sorted(words, key=cmp_to_key(compare_length))
print(sorted_words)  # ['!', 'is', 'python', 'awesome']

# Exemple: tri custom complexe
def compare_custom(a, b):
    # Trier par longueur, puis alphabétique
    if len(a) != len(b):
        return len(a) - len(b)
    if a < b:
        return -1
    elif a > b:
        return 1
    return 0

words = ["cat", "elephant", "dog", "ant", "bear"]
sorted_words = sorted(words, key=cmp_to_key(compare_custom))
print(sorted_words)  # ['ant', 'cat', 'dog', 'bear', 'elephant']


[OK] EXEMPLES PRATIQUES COMBINÉS


# 1. Cache avec timeout (lru_cache + time)
import time
from functools import lru_cache, wraps

def timed_lru_cache(seconds: int, maxsize: int = 128):
    def wrapper(func):
        @lru_cache(maxsize=maxsize)
        def cached_func(*args, _cache_time=None, **kwargs):
            return func(*args, **kwargs)
        
        @wraps(func)
        def inner(*args, **kwargs):
            current_time = int(time.time() / seconds)
            return cached_func(*args, _cache_time=current_time, **kwargs)
        
        return inner
    return wrapper

@timed_lru_cache(seconds=10)
def get_data(x):
    print(f"Fetching data for {x}")
    return x * 2


# 2. Mémoization avec functools
class Memoize:
    def __init__(self, func):
        self.func = func
        self.cache = {}
        wraps(func)(self)
    
    def __call__(self, *args):
        if args not in self.cache:
            self.cache[args] = self.func(*args)
        return self.cache[args]

@Memoize
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)


# 3. Pipeline de fonctions avec reduce
from functools import reduce

def compose(*functions):
    """Compose functions: compose(f, g, h)(x) = f(g(h(x)))"""
    return reduce(lambda f, g: lambda x: f(g(x)), functions)

add_10 = lambda x: x + 10
multiply_2 = lambda x: x * 2
subtract_5 = lambda x: x - 5

pipeline = compose(subtract_5, multiply_2, add_10)
result = pipeline(5)  # ((5 + 10) * 2) - 5 = 25


# 4. Decorator avec état via partial
from functools import partial, wraps

def counted(func=None, *, prefix="Call"):
    if func is None:
        return partial(counted, prefix=prefix)
    
    @wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        print(f"{prefix} #{wrapper.calls}")
        return func(*args, **kwargs)
    
    wrapper.calls = 0
    return wrapper

@counted(prefix="Execution")
def process():
    pass


# 5. Dispatch par type multiple
from functools import singledispatch

@singledispatch
def handle_data(data):
    raise NotImplementedError(f"No handler for {type(data)}")

@handle_data.register(str)
def _(data):
    return data.strip().upper()

@handle_data.register(int)
def _(data):
    return data * 2

@handle_data.register(list)
def _(data):
    return [handle_data(item) for item in data]

@handle_data.register(dict)
def _(data):
    return {k: handle_data(v) for k, v in data.items()}


# 6. Rate limiter avec lru_cache
import time
from functools import wraps

def rate_limit(max_per_second):
    min_interval = 1.0 / max_per_second
    def decorator(func):
        last_called = [0.0]
        
        @wraps(func)
        def wrapper(*args, **kwargs):
            elapsed = time.time() - last_called[0]
            wait_time = min_interval - elapsed
            if wait_time > 0:
                time.sleep(wait_time)
            last_called[0] = time.time()
            return func(*args, **kwargs)
        
        return wrapper
    return decorator

@rate_limit(max_per_second=2)
def api_call():
    print("API called")


# 7. Retry avec exponentiel backoff
from functools import wraps
import time

def retry(max_attempts=3, delay=1, backoff=2):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            attempt = 0
            current_delay = delay
            while attempt < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    attempt += 1
                    if attempt >= max_attempts:
                        raise
                    time.sleep(current_delay)
                    current_delay *= backoff
        return wrapper
    return decorator

@retry(max_attempts=3, delay=1, backoff=2)
def unstable_function():
    import random
    if random.random() < 0.7:
        raise Exception("Random failure")
    return "Success"


[OK] PERFORMANCE & BONNES PRATIQUES


# [OK] Utiliser @cache/@lru_cache pour fonctions pures coûteuses
# [OK] Utiliser @wraps dans tous les décorateurs
# [OK] @singledispatch pour éviter if/elif/else sur types
# [OK] @total_ordering pour classes comparables
# [OK] partial pour callbacks et configuration
# [OK] cached_property pour propriétés coûteuses
# [OK] Vider cache avec .cache_clear() si nécessaire

# [X] Ne pas cacher fonctions avec side-effects
# [X] Attention à maxsize=None (mémoire illimitée)
# [X] Ne pas oublier typed=True si types différents
# [X] reduce peut être moins lisible que loop explicite
# [X] @lru_cache incompatible avec kwargs non-hashables


[OK] PATTERNS AVANCÉS


# 1. Curry partiel
from functools import partial

def curry(func):
    """Auto-curry une fonction"""
    def curried(*args, **kwargs):
        if len(args) + len(kwargs) >= func.__code__.co_argcount:
            return func(*args, **kwargs)
        return partial(curried, *args, **kwargs)
    return curried

@curry
def add3(a, b, c):
    return a + b + c

result = add3(1)(2)(3)  # 6
result = add3(1, 2)(3)  # 6


# 2. Singleton avec lru_cache
from functools import lru_cache

@lru_cache(maxsize=1)
def get_singleton():
    class Singleton:
        pass
    return Singleton()

instance1 = get_singleton()
instance2 = get_singleton()
assert instance1 is instance2  # True


# 3. Property avec cache temporaire
from functools import cached_property
import time

class TimedCache:
    def __init__(self, ttl):
        self.ttl = ttl
        self._cache = {}
    
    def __call__(self, func):
        @wraps(func)
        def wrapper(instance):
            now = time.time()
            if func.__name__ in self._cache:
                value, timestamp = self._cache[func.__name__]
                if now - timestamp < self.ttl:
                    return value
            
            value = func(instance)
            self._cache[func.__name__] = (value, now)
            return value
        return property(wrapper)


# 4. Chaining avec partial
from functools import partial

class Chainable:
    def __init__(self, value):
        self.value = value
    
    def map(self, func):
        return Chainable(func(self.value))
    
    def filter(self, predicate):
        if predicate(self.value):
            return self
        return Chainable(None)
    
    def get(self):
        return self.value

result = (Chainable([1, 2, 3, 4, 5])
    .map(partial(map, lambda x: x * 2))
    .map(list)
    .map(partial(filter, lambda x: x > 5))
    .map(list)
    .get())


[OK] DEBUGGING & INTROSPECTION


from functools import lru_cache, wraps

@lru_cache(maxsize=128)
def cached_func(x):
    return x * 2

# Informations cache
print(cached_func.cache_info())
# CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)

# Statistiques
cached_func(5)
cached_func(5)
cached_func(10)
print(cached_func.cache_info())
# CacheInfo(hits=1, misses=2, maxsize=128, currsize=2)

# Accéder fonction originale
original = cached_func.__wrapped__
print(original(5))  # Pas de cache

# Vider cache
cached_func.cache_clear()

# Pour singledispatch
from functools import singledispatch

@singledispatch
def func(arg):
    pass

# Voir les implémentations
print(func.registry)  # Mapping des types
print(func.registry.keys())  # Types enregistrés


[OK] RESSOURCES


# Documentation: https://docs.python.org/3/library/functools.html
# PEP 443: Single-dispatch generic functions
# PEP 309: Partial Function Application
# Real Python functools guide
# Python Cookbook (3rd Edition)