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


[OK] INTRODUCTION & IMPORTS

# Disponible depuis Python 3.7+
from dataclasses import dataclass

# Imports avancés
from dataclasses import (
    dataclass,
    field,
    fields,
    asdict,
    astuple,
    make_dataclass,
    replace,
    is_dataclass,
    FrozenInstanceError,
    InitVar,
    MISSING,
)

# Imports pour types
from typing import List, Dict, Optional, ClassVar, Any
from typing import InitVar as TypingInitVar  # Alternative


[OK] DATACLASS BASIQUE

# Classe traditionnelle
class PersonOld:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age
    
    def __repr__(self):
        return f"PersonOld(name={self.name!r}, age={self.age!r})"
    
    def __eq__(self, other):
        if not isinstance(other, PersonOld):
            return NotImplemented
        return (self.name, self.age) == (other.name, other.age)

# Avec dataclass (équivalent)
@dataclass
class Person:
    name: str
    age: int

# Utilisation
person = Person("Alice", 30)
print(person)                   # Person(name='Alice', age=30)
print(person.name)              # Alice
print(person.age)               # 30

# Comparaison
person1 = Person("Alice", 30)
person2 = Person("Alice", 30)
print(person1 == person2)       # True


[OK] PARAMÈTRES DU DÉCORATEUR @dataclass

@dataclass(
    init=True,              # Génère __init__ (défaut: True)
    repr=True,              # Génère __repr__ (défaut: True)
    eq=True,                # Génère __eq__ (défaut: True)
    order=False,            # Génère __lt__, __le__, __gt__, __ge__ (défaut: False)
    unsafe_hash=False,      # Génère __hash__ (défaut: False)
    frozen=False,           # Rend instance immuable (défaut: False)
    match_args=True,        # Génère __match_args__ pour pattern matching (3.10+)
    kw_only=False,          # Force keyword-only arguments (3.10+)
    slots=False,            # Génère __slots__ (3.10+)
    weakref_slot=False,     # Ajoute __weakref__ à slots (3.11+)
)
class Example:
    field: str


# === init=True (défaut) ===
@dataclass
class WithInit:
    name: str
    age: int

obj = WithInit("Alice", 30)     # [OK] Fonctionne

@dataclass(init=False)
class WithoutInit:
    name: str
    age: int
    
    def __init__(self, full_name: str):
        parts = full_name.split()
        self.name = parts[0]
        self.age = 0

obj = WithoutInit("Alice Smith")  # __init__ personnalisé


# === repr=True (défaut) ===
@dataclass
class WithRepr:
    name: str
    age: int

print(WithRepr("Alice", 30))    # WithRepr(name='Alice', age=30)

@dataclass(repr=False)
class WithoutRepr:
    name: str
    age: int

print(WithoutRepr("Alice", 30))  # <__main__.WithoutRepr object at 0x...>


# === eq=True (défaut) ===
@dataclass
class WithEq:
    name: str
    age: int

a = WithEq("Alice", 30)
b = WithEq("Alice", 30)
print(a == b)                   # True

@dataclass(eq=False)
class WithoutEq:
    name: str
    age: int

a = WithoutEq("Alice", 30)
b = WithoutEq("Alice", 30)
print(a == b)                   # False (comparaison d'identité)


# === order=True ===
@dataclass(order=True)
class SortablePerson:
    name: str
    age: int

people = [
    SortablePerson("Charlie", 35),
    SortablePerson("Alice", 30),
    SortablePerson("Bob", 25),
]
sorted_people = sorted(people)
# Trie par (name, age) dans l'ordre des champs

# ATTENTION: order=True nécessite eq=True
@dataclass(order=True, eq=True)  # [OK] OK
class Valid:
    x: int


# === unsafe_hash=True ===
@dataclass(unsafe_hash=True)
class Hashable:
    name: str
    age: int

person = Hashable("Alice", 30)
person_set = {person}           # [OK] Peut être dans un set
person_dict = {person: "value"} # [OK] Peut être clé de dict

# ATTENTION: unsafe_hash avec champs mutables = danger!
person.age = 31                 # Hash change!


# === frozen=True (immuable) ===
@dataclass(frozen=True)
class ImmutablePerson:
    name: str
    age: int

person = ImmutablePerson("Alice", 30)
# person.age = 31               # [X] FrozenInstanceError!

# frozen=True génère automatiquement __hash__
person_set = {person}           # [OK] Hashable automatiquement


# === match_args=True (Python 3.10+) ===
@dataclass(match_args=True)
class Point:
    x: int
    y: int

point = Point(10, 20)

# Pattern matching
match point:
    case Point(0, 0):
        print("Origin")
    case Point(x, 0):
        print(f"X-axis: {x}")
    case Point(0, y):
        print(f"Y-axis: {y}")
    case Point(x, y):
        print(f"Point: ({x}, {y})")


# === kw_only=True (Python 3.10+) ===
@dataclass(kw_only=True)
class KeywordOnly:
    name: str
    age: int

# obj = KeywordOnly("Alice", 30)    # [X] TypeError!
obj = KeywordOnly(name="Alice", age=30)  # [OK] OK


# === slots=True (Python 3.10+) ===
@dataclass(slots=True)
class Optimized:
    name: str
    age: int

# Avantages:
# - Utilise moins de mémoire
# - Accès aux attributs plus rapide
# - Empêche création dynamique d'attributs

obj = Optimized("Alice", 30)
# obj.new_attr = "value"        # [X] AttributeError!


[OK] VALEURS PAR DÉFAUT

# Valeurs par défaut simples
@dataclass
class Person:
    name: str
    age: int = 0
    active: bool = True

person1 = Person("Alice")
person2 = Person("Bob", 25)
person3 = Person("Charlie", 35, False)


# ATTENTION: Valeurs mutables interdites!
@dataclass
class Wrong:
    name: str
    # items: list = []          # [X] ValueError!
    # tags: dict = {}           # [X] ValueError!


# Solution: utiliser field(default_factory=...)
@dataclass
class Correct:
    name: str
    items: list = field(default_factory=list)
    tags: dict = field(default_factory=dict)
    
person1 = Correct("Alice")
person2 = Correct("Bob")
person1.items.append("item1")
print(person1.items)            # ['item1']
print(person2.items)            # [] - Listes indépendantes!


# default_factory avec lambda
@dataclass
class WithCustomDefaults:
    name: str
    timestamp: float = field(default_factory=lambda: time.time())
    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    counter: int = field(default_factory=lambda: random.randint(1, 100))


# default_factory avec fonction
def get_default_config():
    return {"debug": False, "timeout": 30}

@dataclass
class App:
    name: str
    config: dict = field(default_factory=get_default_config)


[OK] FONCTION field() - OPTIONS DÉTAILLÉES

from dataclasses import field, MISSING

@dataclass
class Example:
    # Syntaxe complète
    field_name: type = field(
        default=MISSING,            # Valeur par défaut
        default_factory=MISSING,    # Fonction pour valeur par défaut
        init=True,                  # Inclure dans __init__
        repr=True,                  # Inclure dans __repr__
        compare=True,               # Utiliser pour comparaisons
        hash=None,                  # Utiliser pour __hash__
        metadata=None,              # Métadonnées arbitraires
        kw_only=False,              # Keyword-only (3.10+)
    )


# === default vs default_factory ===
@dataclass
class Defaults:
    # Valeurs immuables: default
    name: str = field(default="Unknown")
    age: int = field(default=0)
    active: bool = field(default=True)
    
    # Valeurs mutables: default_factory
    items: list = field(default_factory=list)
    tags: dict = field(default_factory=dict)
    data: set = field(default_factory=set)


# === init=False ===
@dataclass
class WithDerivedField:
    first_name: str
    last_name: str
    full_name: str = field(init=False)
    
    def __post_init__(self):
        self.full_name = f"{self.first_name} {self.last_name}"

person = WithDerivedField("Alice", "Smith")
# person = WithDerivedField("Alice", "Smith", "Alice Smith")  # [X] Erreur!
print(person.full_name)         # Alice Smith


# === repr=False ===
@dataclass
class WithSecrets:
    username: str
    password: str = field(repr=False)
    email: str

user = WithSecrets("alice", "secret123", "alice@example.com")
print(user)  # WithSecrets(username='alice', email='alice@example.com')
# Password n'apparaît pas!


# === compare=False ===
@dataclass
class Person:
    name: str
    age: int
    internal_id: str = field(compare=False)

person1 = Person("Alice", 30, "id1")
person2 = Person("Alice", 30, "id2")
print(person1 == person2)       # True (internal_id ignoré)


# === hash=True/False/None ===
@dataclass(unsafe_hash=True)
class Hashable:
    name: str
    age: int
    metadata: dict = field(hash=False)  # Exclu du hash

# hash=None (défaut): utilise compare
# hash=True: inclut dans hash
# hash=False: exclut du hash


# === metadata ===
@dataclass
class User:
    username: str = field(metadata={"description": "User login name"})
    email: str = field(metadata={"description": "Email address", "required": True})
    age: int = field(metadata={"min": 0, "max": 150})

# Accès aux métadonnées
from dataclasses import fields

for f in fields(User):
    print(f.name, f.metadata)
# username {'description': 'User login name'}
# email {'description': 'Email address', 'required': True}
# age {'min': 0, 'max': 150}


# === kw_only=True (Python 3.10+) ===
@dataclass
class Mixed:
    name: str                           # Positionnel
    age: int                            # Positionnel
    email: str = field(kw_only=True)    # Keyword-only
    active: bool = field(default=True, kw_only=True)

obj = Mixed("Alice", 30, email="alice@example.com")
# obj = Mixed("Alice", 30, "alice@example.com")  # [X] Erreur!


[OK] __post_init__ - INITIALISATION PERSONNALISÉE

# Validation
@dataclass
class Person:
    name: str
    age: int
    
    def __post_init__(self):
        if self.age < 0:
            raise ValueError("Age cannot be negative")
        if not self.name:
            raise ValueError("Name cannot be empty")


# Calculs dérivés
@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)
    perimeter: float = field(init=False)
    
    def __post_init__(self):
        self.area = self.width * self.height
        self.perimeter = 2 * (self.width + self.height)


# Transformation de données
@dataclass
class User:
    name: str
    email: str
    
    def __post_init__(self):
        self.name = self.name.strip().title()
        self.email = self.email.lower().strip()


# Champs privés
@dataclass
class BankAccount:
    account_number: str
    _balance: float = field(default=0.0, repr=False)
    
    def __post_init__(self):
        if self._balance < 0:
            raise ValueError("Initial balance cannot be negative")


[OK] InitVar - VARIABLES D'INITIALISATION

# InitVar: paramètres __init__ qui ne deviennent pas des attributs
from dataclasses import InitVar

@dataclass
class Database:
    host: str
    port: int
    database: str = field(init=False)
    password: InitVar[str] = None
    
    def __post_init__(self, password: str):
        # password est disponible ici mais ne devient pas attribut
        if password:
            self.database = f"Connecting to {self.host}:{self.port}"
        else:
            self.database = f"No password for {self.host}"

db = Database("localhost", 5432, password="secret")
print(db.database)
# print(db.password)            # [X] AttributeError!


# Exemple: configuration complexe
@dataclass
class Rectangle:
    width: float = field(init=False)
    height: float = field(init=False)
    diagonal: InitVar[float] = None
    aspect_ratio: InitVar[float] = None
    
    def __post_init__(self, diagonal: float, aspect_ratio: float):
        if diagonal and aspect_ratio:
            # Calcul width et height depuis diagonal et ratio
            self.width = diagonal / (1 + aspect_ratio**2)**0.5
            self.height = self.width * aspect_ratio

rect = Rectangle(diagonal=10, aspect_ratio=1.5)


# Exemple: validation multi-champs
@dataclass
class DateRange:
    start_date: str
    end_date: str
    strict: InitVar[bool] = True
    
    def __post_init__(self, strict: bool):
        start = datetime.fromisoformat(self.start_date)
        end = datetime.fromisoformat(self.end_date)
        
        if strict and start > end:
            raise ValueError("start_date must be before end_date")


[OK] HÉRITAGE

# Héritage simple
@dataclass
class Person:
    name: str
    age: int

@dataclass
class Employee(Person):
    employee_id: str
    salary: float

emp = Employee("Alice", 30, "E123", 50000)
print(emp)
# Employee(name='Alice', age=30, employee_id='E123', salary=50000)


# Ordre des champs: classe parente d'abord
@dataclass
class Base:
    x: int
    y: int = 10

@dataclass
class Derived(Base):
    z: int = 20

# Ordre effectif: x, y, z
obj = Derived(x=1, y=2, z=3)


# ATTENTION: Champs avec défaut avant champs sans défaut
@dataclass
class Base:
    x: int = 10

@dataclass
class Derived(Base):
    pass
    # y: int                    # [X] Erreur! Champ sans défaut après x

# Solution: mettre défaut ou utiliser kw_only
@dataclass
class Derived(Base):
    y: int = field(kw_only=True)  # [OK] OK


# Héritage multiple
@dataclass
class A:
    x: int

@dataclass
class B:
    y: int

@dataclass
class C(A, B):
    z: int

obj = C(x=1, y=2, z=3)


# Surcharge de méthodes
@dataclass
class Person:
    name: str
    age: int
    
    def greet(self):
        return f"Hello, I'm {self.name}"

@dataclass
class Employee(Person):
    employee_id: str
    
    def greet(self):
        return f"Hello, I'm {self.name}, employee {self.employee_id}"


# Classe abstraite
from abc import ABC, abstractmethod

@dataclass
class Shape(ABC):
    color: str
    
    @abstractmethod
    def area(self) -> float:
        pass

@dataclass
class Circle(Shape):
    radius: float
    
    def area(self) -> float:
        return 3.14159 * self.radius ** 2


[OK] MÉTHODES UTILITAIRES

# === asdict() - Conversion en dictionnaire ===
from dataclasses import asdict

@dataclass
class Person:
    name: str
    age: int
    email: str

person = Person("Alice", 30, "alice@example.com")
person_dict = asdict(person)
print(person_dict)
# {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}

# Avec structures imbriquées
@dataclass
class Address:
    street: str
    city: str

@dataclass
class Person:
    name: str
    address: Address

person = Person("Alice", Address("123 Main St", "NYC"))
person_dict = asdict(person)
# {'name': 'Alice', 'address': {'street': '123 Main St', 'city': 'NYC'}}

# dict_factory pour personnalisation
person_ordered = asdict(person, dict_factory=OrderedDict)


# === astuple() - Conversion en tuple ===
from dataclasses import astuple

person = Person("Alice", 30, "alice@example.com")
person_tuple = astuple(person)
print(person_tuple)             # ('Alice', 30, 'alice@example.com')

# Unpacking
name, age, email = astuple(person)

# Avec tuple_factory
from collections import namedtuple
PersonTuple = namedtuple('PersonTuple', ['name', 'age', 'email'])
person_named = astuple(person, tuple_factory=PersonTuple)


# === replace() - Copie avec modifications ===
from dataclasses import replace

person = Person("Alice", 30, "alice@example.com")
person2 = replace(person, age=31)
print(person)                   # Person(name='Alice', age=30, ...)
print(person2)                  # Person(name='Alice', age=31, ...)

# Équivalent à:
# person2 = Person(person.name, 31, person.email)

# Utile pour objets frozen
@dataclass(frozen=True)
class ImmutablePerson:
    name: str
    age: int

person = ImmutablePerson("Alice", 30)
# person.age = 31               # [X] Erreur!
person2 = replace(person, age=31)  # [OK] OK


# === fields() - Introspection ===
from dataclasses import fields

@dataclass
class Person:
    name: str
    age: int = 0
    email: str = field(default="")

for f in fields(Person):
    print(f"Name: {f.name}")
    print(f"Type: {f.type}")
    print(f"Default: {f.default}")
    print(f"Default factory: {f.default_factory}")
    print()

# Accès aux métadonnées
field_info = fields(Person)[0]
print(field_info.name)          # 'name'
print(field_info.type)          # <class 'str'>


# === is_dataclass() - Vérification ===
from dataclasses import is_dataclass

@dataclass
class Person:
    name: str

class NotDataclass:
    pass

print(is_dataclass(Person))     # True
print(is_dataclass(Person("Alice")))  # True
print(is_dataclass(NotDataclass))  # False


# === make_dataclass() - Création dynamique ===
from dataclasses import make_dataclass

# Création simple
Person = make_dataclass('Person', ['name', 'age'])
person = Person("Alice", 30)

# Avec types
Person = make_dataclass('Person', [('name', str), ('age', int)])

# Avec valeurs par défaut
Person = make_dataclass(
    'Person',
    [('name', str), ('age', int, 0), ('email', str, '')]
)

# Avec field()
Person = make_dataclass(
    'Person',
    [
        ('name', str),
        ('age', int),
        ('tags', list, field(default_factory=list))
    ]
)

# Avec paramètres dataclass
Person = make_dataclass(
    'Person',
    [('name', str), ('age', int)],
    frozen=True,
    order=True
)

# Avec méthodes
def greet(self):
    return f"Hello, {self.name}"

Person = make_dataclass(
    'Person',
    [('name', str), ('age', int)],
    namespace={'greet': greet}
)


[OK] ClassVar - VARIABLES DE CLASSE

from typing import ClassVar

@dataclass
class Employee:
    name: str
    age: int
    
    # Variable de classe (partagée)
    company: ClassVar[str] = "Acme Corp"
    employee_count: ClassVar[int] = 0
    
    def __post_init__(self):
        Employee.employee_count += 1

emp1 = Employee("Alice", 30)
emp2 = Employee("Bob", 25)

print(Employee.company)         # Acme Corp
print(Employee.employee_count)  # 2

# ClassVar n'est PAS dans __init__
# emp = Employee("Alice", 30, "Other Corp")  # [X] Erreur!


# Exemple: configuration partagée
@dataclass
class APIClient:
    endpoint: str
    
    base_url: ClassVar[str] = "https://api.example.com"
    timeout: ClassVar[int] = 30
    retry_count: ClassVar[int] = 3

client = APIClient("/users")
url = f"{APIClient.base_url}{client.endpoint}"


[OK] FROZEN (IMMUABILITÉ)

@dataclass(frozen=True)
class Point:
    x: int
    y: int

point = Point(10, 20)
# point.x = 15                  # [X] FrozenInstanceError!

# Hashable automatiquement
points = {point}
point_dict = {point: "value"}


# Modification via replace()
point2 = replace(point, x=15)   # [OK] OK


# Frozen avec champs mutables (attention!)
@dataclass(frozen=True)
class Container:
    items: list = field(default_factory=list)

container = Container()
# container.items = []          # [X] FrozenInstanceError!
container.items.append("item")  # [OK] OK (liste elle-même mutable!)

# Pour vraie immuabilité: tuple
@dataclass(frozen=True)
class TrulyImmutable:
    items: tuple = ()


# Frozen et héritage
@dataclass(frozen=True)
class FrozenBase:
    x: int

@dataclass(frozen=True)
class FrozenDerived(FrozenBase):
    y: int

# Mélanger frozen et non-frozen: possible mais déconseillé
@dataclass
class MutableDerived(FrozenBase):  # Possible mais bizarre
    y: int


[OK] SLOTS (OPTIMISATION)

# Python 3.10+
@dataclass(slots=True)
class OptimizedPoint:
    x: int
    y: int

point = OptimizedPoint(10, 20)

# Avantages:
# 1. ~40% moins de mémoire
# 2. Accès attributs ~20% plus rapide
# 3. Empêche attributs dynamiques

# point.z = 30                  # [X] AttributeError!


# Mesure de la mémoire
import sys

@dataclass
class Normal:
    x: int
    y: int

@dataclass(slots=True)
class WithSlots:
    x: int
    y: int

normal = Normal(10, 20)
with_slots = WithSlots(10, 20)

print(sys.getsizeof(normal.__dict__))      # ~240 bytes
print(sys.getsizeof(with_slots))           # ~64 bytes


# Slots et héritage
@dataclass(slots=True)
class Base:
    x: int

@dataclass(slots=True)
class Derived(Base):
    y: int

# ATTENTION: tous les niveaux doivent avoir slots=True


# Slots et __dict__ (Python 3.11+)
@dataclass(slots=True)
class WithDictSlot:
    __slots__ = ('__dict__',)
    x: int

obj = WithDictSlot(10)
obj.dynamic = "value"           # [OK] OK maintenant


[OK] PATTERN MATCHING (Python 3.10+)

@dataclass
class Point:
    x: int
    y: int

@dataclass
class Circle:
    center: Point
    radius: float

# Pattern matching simple
point = Point(10, 20)

match point:
    case Point(0, 0):
        print("Origin")
    case Point(x, 0):
        print(f"On X-axis at {x}")
    case Point(0, y):
        print(f"On Y-axis at {y}")
    case Point(x, y) if x == y:
        print(f"Diagonal at {x}")
    case Point(x, y):
        print(f"Point at ({x}, {y})")


# Pattern matching imbriqué
shape = Circle(Point(0, 0), 5)

match shape:
    case Circle(Point(0, 0), radius):
        print(f"Circle at origin with radius {radius}")
    case Circle(center, radius) if radius > 10:
        print(f"Large circle at {center}")
    case Circle(center, radius):
        print(f"Circle at {center} with radius {radius}")


# match_args=False pour désactiver
@dataclass(match_args=False)
class NoPatternMatch:
    x: int
    y: int

# Impossible d'utiliser pattern matching positionnel


[OK] EXEMPLES PRATIQUES

# === Configuration ===
@dataclass
class DatabaseConfig:
    host: str = "localhost"
    port: int = 5432
    database: str = "mydb"
    username: str = ""
    password: str = field(default="", repr=False)
    pool_size: int = 5
    timeout: int = 30

config = DatabaseConfig(
    host="prod.example.com",
    username="admin",
    password="secret"
)


# === Point avec opérations ===
@dataclass
class Point:
    x: float
    y: float
    
    def distance_to(self, other: 'Point') -> float:
        return ((self.x - other.x)**2 + (self.y - other.y)**2)**0.5
    
    def __add__(self, other: 'Point') -> 'Point':
        return Point(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other: 'Point') -> 'Point':
        return Point(self.x - other.x, self.y - other.y)
    
    def __mul__(self, scalar: float) -> 'Point':
        return Point(self.x * scalar, self.y * scalar)

p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1 + p2)                  # Point(x=4, y=6)
print(p1.distance_to(p2))       # 2.8284...


# === JSON sérialization ===
import json
from dataclasses import asdict

@dataclass
class User:
    id: int
    username: str
    email: str
    active: bool = True

user = User(1, "alice", "alice@example.com")

# Vers JSON
json_str = json.dumps(asdict(user), indent=2)

# Depuis JSON
data = json.loads(json_str)
user2 = User(**data)


# === Builder pattern ===
@dataclass
class Query:
    table: str = ""
    fields: list = field(default_factory=list)
    where: dict = field(default_factory=dict)
    limit: int = 0
    
    def select(self, *fields):
        self.fields.extend(fields)
        return self
    
    def from_table(self, table):
        self.table = table
        return self
    
    def filter(self, **kwargs):
        self.where.update(kwargs)
        return self
    
    def with_limit(self, limit):
        self.limit = limit
        return self

query = (Query()
    .select("id", "name", "email")
    .from_table("users")
    .filter(active=True, age=30)
    .with_limit(10)
)


# === Validation avec properties ===
@dataclass
class Person:
    name: str
    _age: int = field(repr=False)
    
    @property
    def age(self):
        return self._age
    
    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("Age cannot be negative")
        if value > 150:
            raise ValueError("Age too high")
        self._age = value
    
    def __post_init__(self):
        # Valider via property setter
        self.age = self._age


# === Cache avec metadata ===
@dataclass
class CachedProperty:
    expensive_data: Any = field(
        init=False,
        default=None,
        metadata={"cached": True}
    )
    
    def __post_init__(self):
        if self.expensive_data is None:
            self.expensive_data = self._compute_expensive_data()
    
    def _compute_expensive_data(self):
        # Calcul coûteux
        return "computed_value"


# === État avec historique ===
@dataclass
class StatefulObject:
    value: int
    _history: list = field(default_factory=list, init=False, repr=False)
    
    def __post_init__(self):
        self._history.append(self.value)
    
    def update(self, new_value: int):
        self._history.append(new_value)
        object.__setattr__(self, 'value', new_value)
    
    def undo(self):
        if len(self._history) > 1:
            self._history.pop()
            object.__setattr__(self, 'value', self._history[-1])


# === Validation avancée ===
from typing import Callable

@dataclass
class ValidatedField:
    name: str
    age: int
    email: str
    
    _validators: ClassVar[dict] = {}
    
    def __post_init__(self):
        self._validate()
    
    def _validate(self):
        if not self.name or len(self.name) < 2:
            raise ValueError("Name must be at least 2 characters")
        
        if self.age < 0 or self.age > 150:
            raise ValueError("Age must be between 0 and 150")
        
        if '@' not in self.email:
            raise ValueError("Invalid email format")


# === Comparable personnalisé ===
@dataclass(order=False)
class Product:
    name: str
    price: float
    rating: float
    
    def __lt__(self, other):
        # Tri par rating d'abord, puis prix
        return (self.rating, -self.price) < (other.rating, -other.price)
    
    def __le__(self, other):
        return self < other or self == other
    
    def __gt__(self, other):
        return not self <= other
    
    def __ge__(self, other):
        return not self < other


# === Registry pattern ===
@dataclass
class Plugin:
    name: str
    version: str
    enabled: bool = True
    
    _registry: ClassVar[dict] = {}
    
    def __post_init__(self):
        Plugin._registry[self.name] = self
    
    @classmethod
    def get(cls, name: str):
        return cls._registry.get(name)
    
    @classmethod
    def all_plugins(cls):
        return list(cls._registry.values())


# === Context manager ===
@dataclass
class Resource:
    name: str
    connection: Any = None
    
    def __enter__(self):
        self.connection = f"Connected to {self.name}"
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.connection = None
        print(f"Closed {self.name}")

with Resource("database") as db:
    print(db.connection)


# === Iterator ===
@dataclass
class Range:
    start: int
    end: int
    step: int = 1
    
    def __iter__(self):
        current = self.start
        while current < self.end:
            yield current
            current += self.step

for i in Range(0, 10, 2):
    print(i)  # 0, 2, 4, 6, 8


[OK] CONVERSION AVEC AUTRES FORMATS

# === Depuis dict ===
@dataclass
class Person:
    name: str
    age: int
    email: str

data = {"name": "Alice", "age": 30, "email": "alice@example.com"}
person = Person(**data)


# === Vers dict (avec filtrage) ===
@dataclass
class User:
    username: str
    password: str = field(repr=False)
    email: str
    internal_id: str = field(compare=False)
    
    def to_public_dict(self):
        d = asdict(self)
        del d['password']
        del d['internal_id']
        return d


# === JSON avec types personnalisés ===
from datetime import datetime
import json

@dataclass
class Event:
    name: str
    timestamp: datetime
    
    def to_json(self):
        d = asdict(self)
        d['timestamp'] = self.timestamp.isoformat()
        return json.dumps(d)
    
    @classmethod
    def from_json(cls, json_str):
        data = json.loads(json_str)
        data['timestamp'] = datetime.fromisoformat(data['timestamp'])
        return cls(**data)


# === YAML ===
import yaml

@dataclass
class Config:
    host: str
    port: int
    debug: bool

# Vers YAML
config = Config("localhost", 8080, True)
yaml_str = yaml.dump(asdict(config))

# Depuis YAML
data = yaml.safe_load(yaml_str)
config2 = Config(**data)


# === ORM-like ===
@dataclass
class Model:
    id: int = field(default=0, init=False)
    
    def save(self):
        # Simuler sauvegarde en DB
        if self.id == 0:
            self.id = self._generate_id()
        print(f"Saved with id {self.id}")
    
    def _generate_id(self):
        return id(self)

@dataclass
class User(Model):
    username: str
    email: str

user = User("alice", "alice@example.com")
user.save()


[OK] DATACLASS VS ALTERNATIVES

# === Dataclass vs NamedTuple ===

# NamedTuple (immuable)
from typing import NamedTuple

class PersonNT(NamedTuple):
    name: str
    age: int

# Dataclass (mutable par défaut)
@dataclass
class PersonDC:
    name: str
    age: int

# Différences:
# - NamedTuple: immuable, léger, tuple sous-jacent
# - Dataclass: mutable, plus flexible, méthodes personnalisables


# === Dataclass vs attrs ===

# attrs (librairie tierce)
import attr

@attr.s(auto_attribs=True)
class PersonAttrs:
    name: str
    age: int

# Dataclass (standard library)
@dataclass
class PersonDC:
    name: str
    age: int

# attrs offre plus de fonctionnalités mais dataclass est standard


# === Dataclass vs TypedDict ===

# TypedDict (pour annotations uniquement)
from typing import TypedDict

class PersonTD(TypedDict):
    name: str
    age: int

# Utilisation
person: PersonTD = {"name": "Alice", "age": 30}

# TypedDict: dict avec types, pas de classe
# Dataclass: vraie classe avec méthodes


# === Dataclass vs Pydantic ===

# Pydantic (validation avancée)
from pydantic import BaseModel, EmailStr, Field

class PersonPydantic(BaseModel):
    name: str = Field(min_length=2)
    age: int = Field(ge=0, le=150)
    email: EmailStr

# Dataclass (validation manuelle)
@dataclass
class PersonDC:
    name: str
    age: int
    email: str
    
    def __post_init__(self):
        if len(self.name) < 2:
            raise ValueError("Name too short")
        if not 0 <= self.age <= 150:
            raise ValueError("Invalid age")

# Pydantic: validation automatique, sérialization avancée
# Dataclass: plus simple, standard library


[OK] PERFORMANCE & OPTIMISATION

# === Mesure de performance ===
import timeit
from sys import getsizeof

# Classe normale
class NormalClass:
    def __init__(self, x, y):
        self.x = x
        self.y = y

# Dataclass normale
@dataclass
class DataClass:
    x: int
    y: int

# Dataclass avec slots
@dataclass(slots=True)
class DataClassSlots:
    x: int
    y: int

# NamedTuple
from typing import NamedTuple
class NTClass(NamedTuple):
    x: int
    y: int

# Tests
def test_creation(cls):
    return cls(10, 20)

# Mesure création
print("Creation time:")
print("Normal:     ", timeit.timeit(lambda: NormalClass(10, 20), number=1000000))
print("Dataclass:  ", timeit.timeit(lambda: DataClass(10, 20), number=1000000))
print("DC Slots:   ", timeit.timeit(lambda: DataClassSlots(10, 20), number=1000000))
print("NamedTuple: ", timeit.timeit(lambda: NTClass(10, 20), number=1000000))

# Mesure mémoire
print("\nMemory usage:")
print("Normal:     ", getsizeof(NormalClass(10, 20).__dict__))
print("Dataclass:  ", getsizeof(DataClass(10, 20).__dict__))
print("DC Slots:   ", getsizeof(DataClassSlots(10, 20)))
print("NamedTuple: ", getsizeof(NTClass(10, 20)))


# === Optimisation avec __slots__ ===

# Sans slots: ~240 bytes par instance
@dataclass
class Point:
    x: float
    y: float
    z: float

# Avec slots: ~64 bytes par instance
@dataclass(slots=True)
class PointOptimized:
    x: float
    y: float
    z: float

# Gain significatif pour beaucoup d'instances
points = [Point(i, i+1, i+2) for i in range(10000)]
points_opt = [PointOptimized(i, i+1, i+2) for i in range(10000)]


# === Frozen pour hashabilité ===
@dataclass(frozen=True, slots=True)
class OptimalKey:
    x: int
    y: int

# Utilisable comme clé de dict, membre de set
cache = {OptimalKey(1, 2): "value"}
unique_points = {OptimalKey(1, 2), OptimalKey(3, 4)}


[OK] PATTERNS AVANCÉS

# === Factory pattern ===
@dataclass
class User:
    username: str
    email: str
    role: str = "user"
    
    @classmethod
    def create_admin(cls, username: str, email: str):
        return cls(username, email, role="admin")
    
    @classmethod
    def create_guest(cls):
        return cls("guest", "guest@example.com", role="guest")
    
    @classmethod
    def from_dict(cls, data: dict):
        return cls(**data)

admin = User.create_admin("alice", "alice@example.com")
guest = User.create_guest()


# === Singleton pattern ===
@dataclass
class Config:
    host: str = "localhost"
    port: int = 8080
    
    _instance: ClassVar[Optional['Config']] = None
    
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

config1 = Config()
config2 = Config()
print(config1 is config2)  # True


# === Observer pattern ===
@dataclass
class Observable:
    value: int
    _observers: list = field(default_factory=list, init=False, repr=False)
    
    def attach(self, observer: Callable):
        self._observers.append(observer)
    
    def detach(self, observer: Callable):
        self._observers.remove(observer)
    
    def notify(self):
        for observer in self._observers:
            observer(self.value)
    
    def set_value(self, value: int):
        self.value = value
        self.notify()


# === Proxy pattern ===
@dataclass
class LazyLoader:
    _data: Any = field(default=None, init=False, repr=False)
    filename: str = ""
    
    @property
    def data(self):
        if self._data is None:
            self._data = self._load_data()
        return self._data
    
    def _load_data(self):
        # Chargement paresseux
        print(f"Loading {self.filename}...")
        return f"Data from {self.filename}"


# === Strategy pattern ===
@dataclass
class Calculator:
    strategy: Callable[[int, int], int]
    
    def calculate(self, a: int, b: int) -> int:
        return self.strategy(a, b)

add = Calculator(lambda a, b: a + b)
multiply = Calculator(lambda a, b: a * b)

print(add.calculate(5, 3))      # 8
print(multiply.calculate(5, 3)) # 15


# === Memento pattern ===
@dataclass
class Memento:
    state: dict
    timestamp: datetime = field(default_factory=datetime.now)

@dataclass
class Stateful:
    value: int
    name: str
    _history: list = field(default_factory=list, init=False, repr=False)
    
    def save(self) -> Memento:
        state = {"value": self.value, "name": self.name}
        memento = Memento(state)
        self._history.append(memento)
        return memento
    
    def restore(self, memento: Memento):
        self.value = memento.state["value"]
        self.name = memento.state["name"]


# === Composite pattern ===
@dataclass
class Component:
    name: str
    
    def operation(self) -> str:
        return self.name

@dataclass
class Composite(Component):
    children: list = field(default_factory=list)
    
    def add(self, component: Component):
        self.children.append(component)
    
    def operation(self) -> str:
        results = [self.name]
        for child in self.children:
            results.append(child.operation())
        return " > ".join(results)


[OK] GESTION D'ERREURS

# Exception personnalisée
@dataclass
class ValidationError(Exception):
    field: str
    value: Any
    message: str
    
    def __str__(self):
        return f"Validation error on '{self.field}': {self.message} (got {self.value})"


# Validation avec exceptions
@dataclass
class StrictUser:
    username: str
    age: int
    
    def __post_init__(self):
        if not 3 <= len(self.username) <= 20:
            raise ValidationError(
                "username",
                self.username,
                "Username must be between 3 and 20 characters"
            )
        
        if not 0 <= self.age <= 150:
            raise ValidationError(
                "age",
                self.age,
                "Age must be between 0 and 150"
            )

# Utilisation
try:
    user = StrictUser("ab", 30)
except ValidationError as e:
    print(e)  # Validation error on 'username': ...


# Validation douce
@dataclass
class User:
    username: str
    age: int
    errors: list = field(default_factory=list, init=False, repr=False)
    
    def __post_init__(self):
        self.errors = []
        
        if not 3 <= len(self.username) <= 20:
            self.errors.append("Invalid username length")
        
        if not 0 <= self.age <= 150:
            self.errors.append("Invalid age range")
    
    def is_valid(self) -> bool:
        return len(self.errors) == 0


[OK] TESTS UNITAIRES

import unittest
from dataclasses import dataclass, field, asdict, replace

class TestDataclasses(unittest.TestCase):
    
    def setUp(self):
        @dataclass
        class Person:
            name: str
            age: int
            email: str = ""
        
        self.Person = Person
    
    def test_creation(self):
        person = self.Person("Alice", 30)
        self.assertEqual(person.name, "Alice")
        self.assertEqual(person.age, 30)
        self.assertEqual(person.email, "")
    
    def test_equality(self):
        person1 = self.Person("Alice", 30)
        person2 = self.Person("Alice", 30)
        self.assertEqual(person1, person2)
    
    def test_repr(self):
        person = self.Person("Alice", 30)
        expected = "Person(name='Alice', age=30, email='')"
        self.assertEqual(repr(person), expected)
    
    def test_asdict(self):
        person = self.Person("Alice", 30, "alice@example.com")
        expected = {
            "name": "Alice",
            "age": 30,
            "email": "alice@example.com"
        }
        self.assertEqual(asdict(person), expected)
    
    def test_replace(self):
        person = self.Person("Alice", 30)
        person2 = replace(person, age=31)
        self.assertEqual(person2.age, 31)
        self.assertEqual(person.age, 30)


# Tests avec pytest
import pytest

@dataclass
class Calculator:
    value: int = 0
    
    def add(self, n: int):
        self.value += n
        return self
    
    def multiply(self, n: int):
        self.value *= n
        return self

def test_calculator_creation():
    calc = Calculator()
    assert calc.value == 0

def test_calculator_operations():
    calc = Calculator(10)
    calc.add(5).multiply(2)
    assert calc.value == 30

def test_calculator_with_fixture():
    calc = Calculator(100)
    assert calc.value == 100


# Mock avec dataclass
from unittest.mock import Mock

@dataclass
class Service:
    api_client: Any
    
    def get_data(self, id: int):
        return self.api_client.fetch(id)

def test_service():
    mock_client = Mock()
    mock_client.fetch.return_value = {"id": 1, "name": "Test"}
    
    service = Service(mock_client)
    result = service.get_data(1)
    
    mock_client.fetch.assert_called_once_with(1)
    assert result["name"] == "Test"


[OK] TYPE HINTS AVANCÉS

from typing import (
    List, Dict, Optional, Union, Tuple,
    Generic, TypeVar, Protocol, Literal
)

# Génériques
T = TypeVar('T')

@dataclass
class Container(Generic[T]):
    items: List[T] = field(default_factory=list)
    
    def add(self, item: T):
        self.items.append(item)
    
    def get(self, index: int) -> T:
        return self.items[index]

# Utilisation
int_container = Container[int]()
int_container.add(42)

str_container = Container[str]()
str_container.add("hello")


# Union types
@dataclass
class Response:
    data: Union[dict, list, None]
    status: int

# Python 3.10+: syntaxe |
@dataclass
class ModernResponse:
    data: dict | list | None
    status: int


# Optional (None autorisé)
@dataclass
class User:
    username: str
    email: Optional[str] = None
    phone: str | None = None  # Python 3.10+


# Literal
@dataclass
class Config:
    mode: Literal["development", "production", "testing"]
    log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]


# Protocol (duck typing structurel)
from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> str: ...

@dataclass
class Circle:
    radius: float
    
    def draw(self) -> str:
        return f"Circle(radius={self.radius})"

@dataclass
class Square:
    side: float
    
    def draw(self) -> str:
        return f"Square(side={self.side})"

def render(shape: Drawable) -> str:
    return shape.draw()

# Circle et Square implémentent Drawable implicitement


# Forward references
@dataclass
class Node:
    value: int
    left: Optional['Node'] = None
    right: Optional['Node'] = None


# Self type (Python 3.11+)
from typing import Self

@dataclass
class Builder:
    value: int = 0
    
    def add(self, n: int) -> Self:
        self.value += n
        return self
    
    def multiply(self, n: int) -> Self:
        self.value *= n
        return self


[OK] INTÉGRATION AVEC D'AUTRES BIBLIOTHÈQUES

# === SQLAlchemy ===
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from dataclasses import dataclass

Base = declarative_base()

@dataclass
class User(Base):
    __tablename__ = 'users'
    
    id: int = Column(Integer, primary_key=True)
    username: str = Column(String(50))
    email: str = Column(String(100))


# === FastAPI ===
from fastapi import FastAPI
from dataclasses import dataclass, asdict

app = FastAPI()

@dataclass
class Item:
    name: str
    price: float
    description: str = ""

@app.post("/items/")
def create_item(item: Item):
    return asdict(item)


# === Django (avec dataclass_factory) ===
@dataclass
class UserDTO:
    id: int
    username: str
    email: str
    
    @classmethod
    def from_model(cls, user_model):
        return cls(
            id=user_model.id,
            username=user_model.username,
            email=user_model.email
        )


# === Pandas ===
import pandas as pd

@dataclass
class Record:
    id: int
    name: str
    value: float

records = [
    Record(1, "A", 10.5),
    Record(2, "B", 20.3),
    Record(3, "C", 15.7)
]

df = pd.DataFrame([asdict(r) for r in records])


# === marshmallow (sérialisation) ===
from marshmallow import Schema, fields
from marshmallow_dataclass import dataclass as mm_dataclass

@mm_dataclass
class User:
    name: str
    age: int
    email: str

# Auto-génère schema marshmallow
schema = User.Schema()
user = User("Alice", 30, "alice@example.com")
json_data = schema.dump(user)


[OK] DEBUGGING & INTROSPECTION

# Afficher structure complète
from dataclasses import fields, MISSING

@dataclass
class Person:
    name: str
    age: int
    email: str = ""
    tags: list = field(default_factory=list)

for f in fields(Person):
    print(f"Field: {f.name}")
    print(f"  Type: {f.type}")
    print(f"  Default: {f.default}")
    print(f"  Default factory: {f.default_factory}")
    print(f"  Init: {f.init}")
    print(f"  Repr: {f.repr}")
    print(f"  Compare: {f.compare}")
    print(f"  Hash: {f.hash}")
    print(f"  Metadata: {f.metadata}")
    print()

# Output:
# Field: name
#   Type: <class 'str'>
#   Default: <dataclasses._MISSING_TYPE object>
#   Default factory: <dataclasses._MISSING_TYPE object>
#   Init: True
#   Repr: True
#   Compare: True
#   Hash: None
#   Metadata: {}


# Vérifier si champ a une valeur par défaut
from dataclasses import MISSING

def has_default(field_obj):
    return field_obj.default is not MISSING or \
           field_obj.default_factory is not MISSING

for f in fields(Person):
    print(f"{f.name}: has_default = {has_default(f)}")

# name: has_default = False
# age: has_default = False
# email: has_default = True
# tags: has_default = True


# Inspecter instance
@dataclass
class User:
    username: str
    email: str
    active: bool = True

user = User("alice", "alice@example.com")

# Accéder aux champs via __dataclass_fields__
print(User.__dataclass_fields__)
print(User.__dataclass_fields__['username'])

# Valeurs de l'instance
for field in fields(user):
    value = getattr(user, field.name)
    print(f"{field.name} = {value}")


# Pretty print avec pprint
import pprint
from dataclasses import asdict

@dataclass
class Config:
    database: dict = field(default_factory=dict)
    cache: dict = field(default_factory=dict)
    features: list = field(default_factory=list)

config = Config(
    database={"host": "localhost", "port": 5432},
    cache={"ttl": 3600, "max_size": 1000},
    features=["auth", "api", "admin"]
)

pprint.pprint(asdict(config), indent=2, width=80)


# JSON pour debugging
import json

def debug_dataclass(obj):
    """Affiche un dataclass de manière lisible"""
    if not is_dataclass(obj):
        raise ValueError("Object is not a dataclass")
    
    print(f"\n=== {obj.__class__.__name__} ===")
    print(json.dumps(asdict(obj), indent=2, default=str))

debug_dataclass(config)


# Comparer deux instances
def compare_dataclasses(obj1, obj2):
    """Compare deux dataclasses et affiche les différences"""
    if type(obj1) != type(obj2):
        print("Different types!")
        return
    
    dict1 = asdict(obj1)
    dict2 = asdict(obj2)
    
    all_keys = set(dict1.keys()) | set(dict2.keys())
    
    for key in sorted(all_keys):
        val1 = dict1.get(key, "MISSING")
        val2 = dict2.get(key, "MISSING")
        
        if val1 != val2:
            print(f"{key}:")
            print(f"  obj1: {val1}")
            print(f"  obj2: {val2}")


# Tracer les changements
@dataclass
class TrackedObject:
    value: int
    _history: list = field(default_factory=list, init=False, repr=False)
    
    def __setattr__(self, name, value):
        if hasattr(self, '_history') and name != '_history':
            old_value = getattr(self, name, None)
            self._history.append({
                'field': name,
                'old': old_value,
                'new': value,
                'timestamp': datetime.now()
            })
        super().__setattr__(name, value)
    
    def show_history(self):
        for change in self._history:
            print(f"{change['timestamp']}: {change['field']} "
                  f"{change['old']} -> {change['new']}")


# Valider structure à runtime
def validate_dataclass_structure(cls, expected_fields):
    """Vérifie qu'un dataclass a la structure attendue"""
    if not is_dataclass(cls):
        raise ValueError(f"{cls} is not a dataclass")
    
    actual_fields = {f.name: f.type for f in fields(cls)}
    
    for field_name, field_type in expected_fields.items():
        if field_name not in actual_fields:
            raise ValueError(f"Missing field: {field_name}")
        
        if actual_fields[field_name] != field_type:
            raise ValueError(
                f"Field {field_name}: expected {field_type}, "
                f"got {actual_fields[field_name]}"
            )
    
    print(f"[OK] {cls.__name__} structure is valid")

# Usage
validate_dataclass_structure(
    Person,
    {'name': str, 'age': int, 'email': str, 'tags': list}
)


[OK] PROFILING & PERFORMANCE

import timeit
import sys
from memory_profiler import profile

# Comparaison création d'instances
@dataclass
class DataClassNormal:
    x: int
    y: int
    z: int

@dataclass(slots=True)
class DataClassSlots:
    x: int
    y: int
    z: int

class RegularClass:
    def __init__(self, x: int, y: int, z: int):
        self.x = x
        self.y = y
        self.z = z

from typing import NamedTuple

class NamedTupleClass(NamedTuple):
    x: int
    y: int
    z: int

# Benchmark création
def benchmark_creation():
    print("Creation time (1M iterations):")
    
    t = timeit.timeit(
        'DataClassNormal(1, 2, 3)',
        globals=globals(),
        number=1_000_000
    )
    print(f"  DataClass Normal: {t:.3f}s")
    
    t = timeit.timeit(
        'DataClassSlots(1, 2, 3)',
        globals=globals(),
        number=1_000_000
    )
    print(f"  DataClass Slots:  {t:.3f}s")
    
    t = timeit.timeit(
        'RegularClass(1, 2, 3)',
        globals=globals(),
        number=1_000_000
    )
    print(f"  Regular Class:    {t:.3f}s")
    
    t = timeit.timeit(
        'NamedTupleClass(1, 2, 3)',
        globals=globals(),
        number=1_000_000
    )
    print(f"  NamedTuple:       {t:.3f}s")


# Benchmark accès attributs
def benchmark_access():
    obj_dc = DataClassNormal(1, 2, 3)
    obj_slots = DataClassSlots(1, 2, 3)
    obj_reg = RegularClass(1, 2, 3)
    obj_nt = NamedTupleClass(1, 2, 3)
    
    print("\nAttribute access (10M iterations):")
    
    t = timeit.timeit('obj_dc.x', globals=locals(), number=10_000_000)
    print(f"  DataClass Normal: {t:.3f}s")
    
    t = timeit.timeit('obj_slots.x', globals=locals(), number=10_000_000)
    print(f"  DataClass Slots:  {t:.3f}s")
    
    t = timeit.timeit('obj_reg.x', globals=locals(), number=10_000_000)
    print(f"  Regular Class:    {t:.3f}s")
    
    t = timeit.timeit('obj_nt.x', globals=locals(), number=10_000_000)
    print(f"  NamedTuple:       {t:.3f}s")


# Mesure mémoire
def measure_memory():
    print("\nMemory usage (100K instances):")
    
    # DataClass Normal
    objects = [DataClassNormal(i, i+1, i+2) for i in range(100_000)]
    size = sum(sys.getsizeof(obj.__dict__) for obj in objects)
    print(f"  DataClass Normal: {size / 1_000_000:.2f} MB")
    
    # DataClass Slots
    objects = [DataClassSlots(i, i+1, i+2) for i in range(100_000)]
    size = sum(sys.getsizeof(obj) for obj in objects)
    print(f"  DataClass Slots:  {size / 1_000_000:.2f} MB")
    
    # NamedTuple
    objects = [NamedTupleClass(i, i+1, i+2) for i in range(100_000)]
    size = sum(sys.getsizeof(obj) for obj in objects)
    print(f"  NamedTuple:       {size / 1_000_000:.2f} MB")


# Memory profiler pour analyse détaillée
@profile
def create_many_dataclasses():
    """Analyse détaillée de la consommation mémoire"""
    objects = []
    for i in range(10_000):
        obj = DataClassNormal(i, i+1, i+2)
        objects.append(obj)
    return objects

# Exécuter: python -m memory_profiler script.py


# Optimisation: réutilisation d'instances (object pool)
from collections import deque

@dataclass
class PooledObject:
    x: int
    y: int
    _pool: ClassVar[deque] = deque(maxlen=1000)
    
    @classmethod
    def get(cls, x: int, y: int):
        if cls._pool:
            obj = cls._pool.pop()
            obj.x = x
            obj.y = y
            return obj
        return cls(x, y)
    
    def release(self):
        self._pool.append(self)


# Optimisation: __slots__ pour grandes collections
@dataclass(slots=True)
class OptimizedPoint:
    x: float
    y: float
    z: float

# Économie: ~40% de mémoire pour 1M instances
points = [OptimizedPoint(i, i+1, i+2) for i in range(1_000_000)]


[OK] SERIALIZATION AVANCÉE

# JSON avec types personnalisés
from datetime import datetime, date
from decimal import Decimal
from uuid import UUID
import json

class EnhancedJSONEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (datetime, date)):
            return obj.isoformat()
        if isinstance(obj, Decimal):
            return float(obj)
        if isinstance(obj, UUID):
            return str(obj)
        if is_dataclass(obj):
            return asdict(obj)
        return super().default(obj)

@dataclass
class Transaction:
    id: UUID
    amount: Decimal
    timestamp: datetime
    tags: list = field(default_factory=list)

transaction = Transaction(
    id=UUID('12345678-1234-5678-1234-567812345678'),
    amount=Decimal('99.99'),
    timestamp=datetime.now()
)

json_str = json.dumps(transaction, cls=EnhancedJSONEncoder, indent=2)
print(json_str)


# Désérialisation avec validation
from typing import get_type_hints

def from_json(cls, json_str: str):
    """Désérialise JSON vers dataclass avec conversion de types"""
    if not is_dataclass(cls):
        raise ValueError("cls must be a dataclass")
    
    data = json.loads(json_str)
    type_hints = get_type_hints(cls)
    
    # Convertir les types
    for field_name, field_type in type_hints.items():
        if field_name in data:
            value = data[field_name]
            
            # Conversion datetime
            if field_type == datetime and isinstance(value, str):
                data[field_name] = datetime.fromisoformat(value)
            
            # Conversion Decimal
            elif field_type == Decimal and isinstance(value, (int, float, str)):
                data[field_name] = Decimal(str(value))
            
            # Conversion UUID
            elif field_type == UUID and isinstance(value, str):
                data[field_name] = UUID(value)
    
    return cls(**data)


# Pickle pour sérialisation binaire
import pickle

@dataclass
class Data:
    values: list = field(default_factory=list)
    metadata: dict = field(default_factory=dict)

data = Data([1, 2, 3], {"key": "value"})

# Sérialiser
with open('data.pkl', 'wb') as f:
    pickle.dump(data, f)

# Désérialiser
with open('data.pkl', 'rb') as f:
    loaded_data = pickle.load(f)


# MessagePack pour sérialisation compacte
import msgpack

def to_msgpack(obj):
    """Sérialise dataclass en MessagePack"""
    return msgpack.packb(asdict(obj), use_bin_type=True)

def from_msgpack(cls, data: bytes):
    """Désérialise MessagePack vers dataclass"""
    dict_data = msgpack.unpackb(data, raw=False)
    return cls(**dict_data)


# Protocol Buffers (protobuf)
# Définir .proto puis générer classes Python
@dataclass
class User:
    id: int
    username: str
    email: str
    
    def to_protobuf(self):
        # Conversion vers message protobuf
        import user_pb2  # Généré depuis .proto
        return user_pb2.User(
            id=self.id,
            username=self.username,
            email=self.email
        )
    
    @classmethod
    def from_protobuf(cls, pb_user):
        return cls(
            id=pb_user.id,
            username=pb_user.username,
            email=pb_user.email
        )


# YAML avec PyYAML
import yaml

@dataclass
class Config:
    host: str
    port: int
    debug: bool
    features: list = field(default_factory=list)

config = Config("localhost", 8080, True, ["auth", "api"])

# Vers YAML
yaml_str = yaml.dump(asdict(config), default_flow_style=False)
print(yaml_str)

# Depuis YAML
yaml_data = yaml.safe_load(yaml_str)
config2 = Config(**yaml_data)


# TOML (configuration)
import tomli  # Python 3.11: tomllib
import tomli_w

@dataclass
class AppConfig:
    name: str
    version: str
    database: dict = field(default_factory=dict)

config = AppConfig(
    "myapp",
    "1.0.0",
    {"host": "localhost", "port": 5432}
)

# Vers TOML
toml_str = tomli_w.dumps(asdict(config))
print(toml_str)

# Depuis TOML
with open("config.toml", "rb") as f:
    data = tomli.load(f)
    config2 = AppConfig(**data)


# CSV pour dataclasses
import csv
from io import StringIO

@dataclass
class Person:
    name: str
    age: int
    email: str

def to_csv(objects: list):
    """Exporte liste de dataclasses vers CSV"""
    if not objects:
        return ""
    
    output = StringIO()
    writer = csv.DictWriter(output, fieldnames=asdict(objects[0]).keys())
    writer.writeheader()
    
    for obj in objects:
        writer.writerow(asdict(obj))
    
    return output.getvalue()

def from_csv(cls, csv_str: str):
    """Importe CSV vers liste de dataclasses"""
    input_data = StringIO(csv_str)
    reader = csv.DictReader(input_data)
    
    objects = []
    type_hints = get_type_hints(cls)
    
    for row in reader:
        # Convertir types
        for field_name, field_type in type_hints.items():
            if field_name in row:
                if field_type == int:
                    row[field_name] = int(row[field_name])
                elif field_type == float:
                    row[field_name] = float(row[field_name])
                elif field_type == bool:
                    row[field_name] = row[field_name].lower() == 'true'
        
        objects.append(cls(**row))
    
    return objects

# Usage
people = [
    Person("Alice", 30, "alice@example.com"),
    Person("Bob", 25, "bob@example.com"),
]

csv_data = to_csv(people)
loaded_people = from_csv(Person, csv_data)


[OK] VALIDATION AVANCÉE

# Validators personnalisés
from typing import Callable, Any

@dataclass
class ValidatedField:
    name: str
    age: int
    email: str
    
    _validators: ClassVar[dict] = {}
    
    def __post_init__(self):
        # Enregistrer validators
        self._validators = {
            'name': [self._validate_name],
            'age': [self._validate_age],
            'email': [self._validate_email],
        }
        self._validate_all()
    
    def _validate_all(self):
        errors = []
        for field_name, validators in self._validators.items():
            value = getattr(self, field_name)
            for validator in validators:
                try:
                    validator(value)
                except ValueError as e:
                    errors.append(f"{field_name}: {e}")
        
        if errors:
            raise ValueError("\n".join(errors))
    
    def _validate_name(self, value: str):
        if not value or len(value) < 2:
            raise ValueError("Name must be at least 2 characters")
        if not value.replace(" ", "").isalpha():
            raise ValueError("Name must contain only letters")
    
    def _validate_age(self, value: int):
        if not 0 <= value <= 150:
            raise ValueError("Age must be between 0 and 150")
    
    def _validate_email(self, value: str):
        if '@' not in value or '.' not in value.split('@')[-1]:
            raise ValueError("Invalid email format")


# Validation avec decorators
def validate_range(min_val, max_val):
    """Decorator pour valider une plage de valeurs"""
    def decorator(func):
        def wrapper(self, value):
            if not min_val <= value <= max_val:
                raise ValueError(
                    f"{func.__name__} must be between {min_val} and {max_val}"
                )
            return func(self, value)
        return wrapper
    return decorator

def validate_pattern(pattern: str):
    """Decorator pour valider avec regex"""
    import re
    compiled = re.compile(pattern)
    
    def decorator(func):
        def wrapper(self, value):
            if not compiled.match(value):
                raise ValueError(
                    f"{func.__name__} must match pattern {pattern}"
                )
            return func(self, value)
        return wrapper
    return decorator


# Validation avec descriptors
class ValidatedString:
    def __init__(self, min_length=0, max_length=None, pattern=None):
        self.min_length = min_length
        self.max_length = max_length
        self.pattern = pattern
        if pattern:
            import re
            self.compiled_pattern = re.compile(pattern)
    
    def __set_name__(self, owner, name):
        self.name = name
        self.private_name = f'_{name}'
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.private_name)
    
    def __set__(self, obj, value):
        if not isinstance(value, str):
            raise TypeError(f"{self.name} must be a string")
        
        if len(value) < self.min_length:
            raise ValueError(
                f"{self.name} must be at least {self.min_length} characters"
            )
        
        if self.max_length and len(value) > self.max_length:
            raise ValueError(
                f"{self.name} must be at most {self.max_length} characters"
            )
        
        if self.pattern and not self.compiled_pattern.match(value):
            raise ValueError(
                f"{self.name} must match pattern {self.pattern}"
            )
        
        setattr(obj, self.private_name, value)

@dataclass
class User:
    username: ValidatedString = ValidatedString(
        min_length=3,
        max_length=20,
        pattern=r'^[a-zA-Z0-9_]+$'
    )
    email: ValidatedString = ValidatedString(
        pattern=r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
    )


# Validation avec Pydantic-style
from typing import Annotated

def validate_positive(value: int) -> int:
    if value <= 0:
        raise ValueError("Must be positive")
    return value

def validate_email_format(value: str) -> str:
    if '@' not in value:
        raise ValueError("Invalid email")
    return value

@dataclass
class Product:
    name: str
    price: float
    quantity: int
    
    def __post_init__(self):
        self.price = self._validate_positive(self.price, "price")
        self.quantity = self._validate_positive(self.quantity, "quantity")
    
    @staticmethod
    def _validate_positive(value, field_name):
        if value <= 0:
            raise ValueError(f"{field_name} must be positive")
        return value


# Validation conditionnelle
@dataclass
class ConditionalValidation:
    type: str
    value: Any
    
    def __post_init__(self):
        if self.type == "email":
            if '@' not in self.value:
                raise ValueError("Invalid email")
        elif self.type == "phone":
            if not self.value.replace("-", "").isdigit():
                raise ValueError("Invalid phone number")
        elif self.type == "age":
            if not 0 <= self.value <= 150:
                raise ValueError("Invalid age")


# Validation cross-field
@dataclass
class DateRange:
    start_date: date
    end_date: date
    
    def __post_init__(self):
        if self.start_date > self.end_date:
            raise ValueError("start_date must be before end_date")

@dataclass
class Password:
    password: str = field(repr=False)
    confirm_password: str = field(repr=False)
    
    def __post_init__(self):
        if self.password != self.confirm_password:
            raise ValueError("Passwords do not match")
        
        if len(self.password) < 8:
            raise ValueError("Password must be at least 8 characters")
        
        # Supprimer confirm_password après validation
        del self.confirm_password


[OK] MÉTAPROGRAMMATION

# Créer dataclasses dynamiquement
from dataclasses import make_dataclass

def create_model(name: str, field_specs: dict):
    """Crée un dataclass dynamiquement depuis un dict de specs"""
    fields_list = []
    
    for field_name, field_info in field_specs.items():
        if isinstance(field_info, type):
            # Type simple
            fields_list.append((field_name, field_info))
        elif isinstance(field_info, dict):
            # Type avec options
            field_type = field_info.get('type', str)
            default = field_info.get('default', MISSING)
            default_factory = field_info.get('default_factory', MISSING)
            
            if default is not MISSING:
                fields_list.append((field_name, field_type, default))
            elif default_factory is not MISSING:
                fields_list.append((
                    field_name,
                    field_type,
                    field(default_factory=default_factory)
                ))
            else:
                fields_list.append((field_name, field_type))
    
    return make_dataclass(name, fields_list)

# Usage
User = create_model('User', {
    'id': int,
    'username': str,
    'email': str,
    'tags': {'type': list, 'default_factory': list},
    'active': {'type': bool, 'default': True},
})

user = User(1, "alice", "alice@example.com")


# Modifier dataclass après création
def add_method(cls, method_name: str, method: Callable):
    """Ajoute une méthode à un dataclass"""
    setattr(cls, method_name, method)
    return cls

@dataclass
class Point:
    x: int
    y: int

def distance_to_origin(self):
    return (self.x**2 + self.y**2)**0.5

add_method(Point, 'distance_to_origin', distance_to_origin)

point = Point(3, 4)
print(point.distance_to_origin())  # 5.0


# Decorator pour ajouter fonctionnalités
def add_validation(cls):
    """Ajoute validation automatique à un dataclass"""
    original_init = cls.__init__
    
    def new_init(self, *args, **kwargs):
        original_init(self, *args, **kwargs)
        self._validate()
    
    cls.__init__ = new_init
    
    def _validate(self):
        for field_obj in fields(self):
            value = getattr(self, field_obj.name)
            
            # Validation basique de type
            if not isinstance(value, field_obj.type):
                try:
                    # Essayer conversion
                    setattr(self, field_obj.name, field_obj.type(value))
                except:
                    raise TypeError(
                        f"{field_obj.name} must be {field_obj.type}"
                    )
    
    cls._validate = _validate
    return cls

@add_validation
@dataclass
class ValidatedUser:
    name: str
    age: int


# Mixin pour fonctionnalités communes
class JSONMixin:
    def to_json(self) -> str:
        return json.dumps(asdict(self), indent=2, default=str)
    
    @classmethod
    def from_json(cls, json_str: str):
        return cls(**json.loads(json_str))

class DictMixin:
    def to_dict(self) -> dict:
        return asdict(self)
    
    @classmethod
    def from_dict(cls, data: dict):
        return cls(**data)

@dataclass
class User(JSONMixin, DictMixin):
    username: str
    email: str

user = User("alice", "alice@example.com")
json_str = user.to_json()
user2 = User.from_json(json_str)


# Générer dataclasses depuis schéma
def from_json_schema(schema: dict) -> type:
    """Crée un dataclass depuis un JSON Schema"""
    name = schema.get('title', 'GeneratedClass')
    properties = schema.get('properties', {})
    required = set(schema.get('required', []))
    
    fields_list = []
    type_mapping = {
        'string': str,
        'integer': int,
        'number': float,
        'boolean': bool,
        'array': list,
        'object': dict,
    }
    
    for prop_name, prop_schema in properties.items():
        prop_type = type_mapping.get(prop_schema.get('type', 'string'), str)
        
        if prop_name in required:
            fields_list.append((prop_name, prop_type))
        else:
            default = prop_schema.get('default')
            if default is not None:
                fields_list.append((prop_name, prop_type, default))
            else:
                if prop_type in (list, dict, set):
                    fields_list.append((
                        prop_name,
                        prop_type,
                        field(default_factory=prop_type)
                    ))
                else:
                    fields_list.append((prop_name, prop_type, None))
    
    return make_dataclass(name, fields_list)

# Usage
schema = {
    'title': 'User',
    'type': 'object',
    'properties': {
        'username': {'type': 'string'},
        'age': {'type': 'integer'},
        'email': {'type': 'string'},
        'tags': {'type': 'array', 'default': []},
    },
    'required': ['username', 'age']
}

User = from_json_schema(schema)


# Class decorator pour auto-register
_registry = {}

def register_dataclass(cls):
    """Enregistre automatiquement les dataclasses"""
    _registry[cls.__name__] = cls
    return cls

@register_dataclass
@dataclass
class User:
    username: str

@register_dataclass
@dataclass
class Product:
    name: str

def get_dataclass(name: str):
    return _registry.get(name)

# Usage
UserClass = get_dataclass('User')
user = UserClass("alice")


[OK] PATTERNS ARCHITECTURAUX

# Repository Pattern
from abc import ABC, abstractmethod
from typing import List, Optional

@dataclass
class Entity:
    id: int
    created_at: datetime = field(default_factory=datetime.now)
    updated_at: datetime = field(default_factory=datetime.now)

@dataclass
class User(Entity):
    username: str
    email: str

class Repository(ABC):
    @abstractmethod
    def find_by_id(self, id: int) -> Optional[Entity]:
        pass
    
    @abstractmethod
    def find_all(self) -> List[Entity]:
        pass
    
    @abstractmethod
    def save(self, entity: Entity) -> Entity:
        pass
    
    @abstractmethod
    def delete(self, id: int) -> bool:
        pass

class InMemoryRepository(Repository):
    def __init__(self):
        self._storage: dict[int, Entity] = {}
        self._next_id = 1
    
    def find_by_id(self, id: int) -> Optional[Entity]:
        return self._storage.get(id)
    
    def find_all(self) -> List[Entity]:
        return list(self._storage.values())
    
    def save(self, entity: Entity) -> Entity:
        if entity.id == 0:
            entity.id = self._next_id
            self._next_id += 1
        entity.updated_at = datetime.now()
        self._storage[entity.id] = entity
        return entity
    
    def delete(self, id: int) -> bool:
        if id in self._storage:
            del self._storage[id]
            return True
        return False

# Usage
user_repo = InMemoryRepository()
user = User(0, username="alice", email="alice@example.com")
saved_user = user_repo.save(user)
found_user = user_repo.find_by_id(saved_user.id)


# Value Object Pattern
@dataclass(frozen=True)
class Money:
    amount: Decimal
    currency: str
    
    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("Amount cannot be negative")
        if self.currency not in ['USD', 'EUR', 'GBP']:
            raise ValueError(f"Unsupported currency: {self.currency}")
    
    def __add__(self, other: 'Money') -> 'Money':
        if self.currency != other.currency:
            raise ValueError("Cannot add different currencies")
        return Money(self.amount + other.amount, self.currency)
    
    def __sub__(self, other: 'Money') -> 'Money':
        if self.currency != other.currency:
            raise ValueError("Cannot subtract different currencies")
        return Money(self.amount - other.amount, self.currency)
    
    def __mul__(self, multiplier: float) -> 'Money':
        return Money(self.amount * Decimal(str(multiplier)), self.currency)
    
    def __truediv__(self, divisor: float) -> 'Money':
        return Money(self.amount / Decimal(str(divisor)), self.currency)

@dataclass(frozen=True)
class Email:
    address: str
    
    def __post_init__(self):
        if '@' not in self.address:
            raise ValueError("Invalid email address")
        
        local, domain = self.address.split('@', 1)
        if not local or not domain:
            raise ValueError("Invalid email address")
        
        object.__setattr__(self, 'local', local)
        object.__setattr__(self, 'domain', domain)


# Aggregate Pattern
@dataclass
class OrderLine:
    product_id: str
    product_name: str
    quantity: int
    unit_price: Money
    
    def total(self) -> Money:
        return self.unit_price * self.quantity

@dataclass
class Order(Entity):
    customer_id: str
    lines: List[OrderLine] = field(default_factory=list)
    status: str = "pending"
    
    def add_line(self, product_id: str, product_name: str, 
                 quantity: int, unit_price: Money):
        """Aggregate root contrôle les modifications"""
        if self.status != "pending":
            raise ValueError("Cannot modify non-pending order")
        
        # Vérifier si produit déjà dans la commande
        for line in self.lines:
            if line.product_id == product_id:
                # Mettre à jour quantité
                line.quantity += quantity
                return
        
        # Ajouter nouvelle ligne
        line = OrderLine(product_id, product_name, quantity, unit_price)
        self.lines.append(line)
        self.updated_at = datetime.now()
    
    def remove_line(self, product_id: str):
        if self.status != "pending":
            raise ValueError("Cannot modify non-pending order")
        
        self.lines = [l for l in self.lines if l.product_id != product_id]
        self.updated_at = datetime.now()
    
    def total(self) -> Money:
        if not self.lines:
            return Money(Decimal('0'), 'USD')
        
        result = self.lines[0].total()
        for line in self.lines[1:]:
            result = result + line.total()
        return result
    
    def submit(self):
        if not self.lines:
            raise ValueError("Cannot submit empty order")
        if self.status != "pending":
            raise ValueError("Order already submitted")
        
        self.status = "submitted"
        self.updated_at = datetime.now()


# Event Sourcing Pattern
@dataclass(frozen=True)
class Event:
    event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
    timestamp: datetime = field(default_factory=datetime.now)
    aggregate_id: str = ""

@dataclass(frozen=True)
class UserCreated(Event):
    username: str
    email: str

@dataclass(frozen=True)
class UserEmailChanged(Event):
    old_email: str
    new_email: str

@dataclass(frozen=True)
class UserDeactivated(Event):
    reason: str

@dataclass
class EventSourcedUser:
    id: str
    username: str
    email: str
    active: bool = True
    _events: List[Event] = field(default_factory=list, init=False, repr=False)
    
    @classmethod
    def create(cls, id: str, username: str, email: str):
        """Factory method qui génère un événement"""
        user = cls(id, username, email)
        event = UserCreated(aggregate_id=id, username=username, email=email)
        user._events.append(event)
        return user
    
    def change_email(self, new_email: str):
        if new_email == self.email:
            return
        
        event = UserEmailChanged(
            aggregate_id=self.id,
            old_email=self.email,
            new_email=new_email
        )
        self._apply_event(event)
        self._events.append(event)
    
    def deactivate(self, reason: str):
        if not self.active:
            return
        
        event = UserDeactivated(aggregate_id=self.id, reason=reason)
        self._apply_event(event)
        self._events.append(event)
    
    def _apply_event(self, event: Event):
        """Applique un événement à l'état"""
        if isinstance(event, UserEmailChanged):
            self.email = event.new_email
        elif isinstance(event, UserDeactivated):
            self.active = False
    
    @classmethod
    def from_events(cls, events: List[Event]):
        """Reconstruit l'état depuis les événements"""
        if not events:
            raise ValueError("No events provided")
        
        first_event = events[0]
        if not isinstance(first_event, UserCreated):
            raise ValueError("First event must be UserCreated")
        
        user = cls(
            first_event.aggregate_id,
            first_event.username,
            first_event.email
        )
        
        for event in events[1:]:
            user._apply_event(event)
        
        user._events = list(events)
        return user
    
    def get_uncommitted_events(self) -> List[Event]:
        return self._events.copy()
    
    def mark_events_committed(self):
        self._events.clear()


# CQRS Pattern (Command Query Responsibility Segregation)
@dataclass(frozen=True)
class Command:
    """Base pour tous les commands"""
    command_id: str = field(default_factory=lambda: str(uuid.uuid4()))
    timestamp: datetime = field(default_factory=datetime.now)

@dataclass(frozen=True)
class CreateUserCommand(Command):
    username: str
    email: str

@dataclass(frozen=True)
class UpdateUserEmailCommand(Command):
    user_id: str
    new_email: str

@dataclass(frozen=True)
class Query:
    """Base pour tous les queries"""
    pass

@dataclass(frozen=True)
class GetUserByIdQuery(Query):
    user_id: str

@dataclass(frozen=True)
class GetAllUsersQuery(Query):
    limit: int = 100
    offset: int = 0

class CommandHandler(ABC):
    @abstractmethod
    def handle(self, command: Command):
        pass

class CreateUserCommandHandler(CommandHandler):
    def __init__(self, repository: Repository):
        self.repository = repository
    
    def handle(self, command: CreateUserCommand):
        user = User(
            id=0,
            username=command.username,
            email=command.email
        )
        return self.repository.save(user)

class QueryHandler(ABC):
    @abstractmethod
    def handle(self, query: Query):
        pass

class GetUserByIdQueryHandler(QueryHandler):
    def __init__(self, repository: Repository):
        self.repository = repository
    
    def handle(self, query: GetUserByIdQuery):
        return self.repository.find_by_id(query.user_id)


# Specification Pattern
@dataclass
class Specification(ABC):
    @abstractmethod
    def is_satisfied_by(self, candidate) -> bool:
        pass
    
    def and_(self, other: 'Specification') -> 'Specification':
        return AndSpecification(self, other)
    
    def or_(self, other: 'Specification') -> 'Specification':
        return OrSpecification(self, other)
    
    def not_(self) -> 'Specification':
        return NotSpecification(self)

@dataclass
class AndSpecification(Specification):
    left: Specification
    right: Specification
    
    def is_satisfied_by(self, candidate) -> bool:
        return (self.left.is_satisfied_by(candidate) and 
                self.right.is_satisfied_by(candidate))

@dataclass
class OrSpecification(Specification):
    left: Specification
    right: Specification
    
    def is_satisfied_by(self, candidate) -> bool:
        return (self.left.is_satisfied_by(candidate) or 
                self.right.is_satisfied_by(candidate))

@dataclass
class NotSpecification(Specification):
    spec: Specification
    
    def is_satisfied_by(self, candidate) -> bool:
        return not self.spec.is_satisfied_by(candidate)

# Spécifications concrètes
@dataclass
class Product:
    name: str
    price: Money
    in_stock: bool
    category: str

@dataclass
class PriceRangeSpecification(Specification):
    min_price: Money
    max_price: Money
    
    def is_satisfied_by(self, product: Product) -> bool:
        return (self.min_price.amount <= product.price.amount <= 
                self.max_price.amount)

@dataclass
class InStockSpecification(Specification):
    def is_satisfied_by(self, product: Product) -> bool:
        return product.in_stock

@dataclass
class CategorySpecification(Specification):
    category: str
    
    def is_satisfied_by(self, product: Product) -> bool:
        return product.category == self.category

# Usage
affordable = PriceRangeSpecification(
    Money(Decimal('0'), 'USD'),
    Money(Decimal('50'), 'USD')
)
in_stock = InStockSpecification()
electronics = CategorySpecification('electronics')

# Combiner specifications
affordable_electronics = affordable.and_(electronics).and_(in_stock)

product = Product("Keyboard", Money(Decimal('30'), 'USD'), True, "electronics")
print(affordable_electronics.is_satisfied_by(product))  # True


[OK] DESIGN PATTERNS AVANCÉS

# Builder Pattern avec dataclass
@dataclass
class QueryBuilder:
    _table: str = field(default="", init=False)
    _fields: List[str] = field(default_factory=list, init=False)
    _where: Dict[str, Any] = field(default_factory=dict, init=False)
    _order_by: List[str] = field(default_factory=list, init=False)
    _limit: Optional[int] = field(default=None, init=False)
    _offset: int = field(default=0, init=False)
    
    def table(self, table_name: str) -> 'QueryBuilder':
        self._table = table_name
        return self
    
    def select(self, *fields: str) -> 'QueryBuilder':
        self._fields.extend(fields)
        return self
    
    def where(self, **conditions) -> 'QueryBuilder':
        self._where.update(conditions)
        return self
    
    def order_by(self, *fields: str) -> 'QueryBuilder':
        self._order_by.extend(fields)
        return self
    
    def limit(self, count: int) -> 'QueryBuilder':
        self._limit = count
        return self
    
    def offset(self, count: int) -> 'QueryBuilder':
        self._offset = count
        return self
    
    def build(self) -> str:
        if not self._table:
            raise ValueError("Table name is required")
        
        fields = ", ".join(self._fields) if self._fields else "*"
        query = f"SELECT {fields} FROM {self._table}"
        
        if self._where:
            conditions = " AND ".join(
                f"{k} = {v!r}" for k, v in self._where.items()
            )
            query += f" WHERE {conditions}"
        
        if self._order_by:
            query += f" ORDER BY {', '.join(self._order_by)}"
        
        if self._limit:
            query += f" LIMIT {self._limit}"
        
        if self._offset:
            query += f" OFFSET {self._offset}"
        
        return query

# Usage
query = (QueryBuilder()
    .table("users")
    .select("id", "username", "email")
    .where(active=True)
    .order_by("created_at")
    .limit(10)
    .build()
)


# Fluent Interface Pattern
@dataclass
class FluentValidator:
    _value: Any = field(default=None, init=False)
    _errors: List[str] = field(default_factory=list, init=False)
    _field_name: str = field(default="value", init=False)
    
    def value(self, val: Any, field_name: str = "value") -> 'FluentValidator':
        self._value = val
        self._field_name = field_name
        return self
    
    def required(self) -> 'FluentValidator':
        if self._value is None or self._value == "":
            self._errors.append(f"{self._field_name} is required")
        return self
    
    def min_length(self, length: int) -> 'FluentValidator':
        if isinstance(self._value, str) and len(self._value) < length:
            self._errors.append(
                f"{self._field_name} must be at least {length} characters"
            )
        return self
    
    def max_length(self, length: int) -> 'FluentValidator':
        if isinstance(self._value, str) and len(self._value) > length:
            self._errors.append(
                f"{self._field_name} must be at most {length} characters"
            )
        return self
    
    def email_format(self) -> 'FluentValidator':
        if isinstance(self._value, str) and '@' not in self._value:
            self._errors.append(f"{self._field_name} must be a valid email")
        return self
    
    def range(self, min_val: float, max_val: float) -> 'FluentValidator':
        if not isinstance(self._value, (int, float)):
            return self
        if not min_val <= self._value <= max_val:
            self._errors.append(
                f"{self._field_name} must be between {min_val} and {max_val}"
            )
        return self
    
    def is_valid(self) -> bool:
        return len(self._errors) == 0
    
    def get_errors(self) -> List[str]:
        return self._errors.copy()

# Usage
validator = FluentValidator()

validator.value("alice", "username").required().min_length(3).max_length(20)
validator.value("alice@example.com", "email").required().email_format()
validator.value(25, "age").required().range(0, 150)

if not validator.is_valid():
    print("Errors:", validator.get_errors())


# Chain of Responsibility Pattern
@dataclass
class Request:
    data: dict
    processed: bool = False

@dataclass
class Handler(ABC):
    next_handler: Optional['Handler'] = None
    
    def set_next(self, handler: 'Handler') -> 'Handler':
        self.next_handler = handler
        return handler
    
    def handle(self, request: Request):
        if self._can_handle(request):
            self._process(request)
        
        if self.next_handler and not request.processed:
            self.next_handler.handle(request)
    
    @abstractmethod
    def _can_handle(self, request: Request) -> bool:
        pass
    
    @abstractmethod
    def _process(self, request: Request):
        pass

@dataclass
class AuthenticationHandler(Handler):
    def _can_handle(self, request: Request) -> bool:
        return 'user_id' not in request.data
    
    def _process(self, request: Request):
        # Simuler authentification
        request.data['user_id'] = 123
        print("Authenticated user")

@dataclass
class ValidationHandler(Handler):
    def _can_handle(self, request: Request) -> bool:
        return 'validated' not in request.data
    
    def _process(self, request: Request):
        # Valider données
        if 'user_id' in request.data:
            request.data['validated'] = True
            print("Request validated")

@dataclass
class ProcessingHandler(Handler):
    def _can_handle(self, request: Request) -> bool:
        return request.data.get('validated', False)
    
    def _process(self, request: Request):
        print("Processing request")
        request.processed = True

# Usage
auth = AuthenticationHandler()
validation = ValidationHandler()
processing = ProcessingHandler()

auth.set_next(validation).set_next(processing)

request = Request(data={})
auth.handle(request)


# State Pattern avec dataclass
@dataclass
class State(ABC):
    @abstractmethod
    def handle(self, context: 'Context'):
        pass

@dataclass
class Context:
    _state: State
    
    def set_state(self, state: State):
        print(f"Changing state to {state.__class__.__name__}")
        self._state = state
    
    def request(self):
        self._state.handle(self)

@dataclass
class ConcreteStateA(State):
    def handle(self, context: Context):
        print("Handling in State A")
        context.set_state(ConcreteStateB())

@dataclass
class ConcreteStateB(State):
    def handle(self, context: Context):
        print("Handling in State B")
        context.set_state(ConcreteStateA())

# Usage
context = Context(ConcreteStateA())
context.request()  # State A -> State B
context.request()  # State B -> State A


# Memento Pattern
@dataclass
class Memento:
    """Sauvegarde de l'état"""
    state: dict
    timestamp: datetime = field(default_factory=datetime.now)

@dataclass
class Originator:
    """Objet dont on sauvegarde l'état"""
    value: int
    name: str
    
    def create_memento(self) -> Memento:
        return Memento(state=asdict(self))
    
    def restore_from_memento(self, memento: Memento):
        for key, value in memento.state.items():
            setattr(self, key, value)

@dataclass
class Caretaker:
    """Gestionnaire des sauvegardes"""
    _mementos: List[Memento] = field(default_factory=list, init=False)
    
    def save(self, memento: Memento):
        self._mementos.append(memento)
    
    def get(self, index: int) -> Memento:
        return self._mementos[index]
    
    def get_latest(self) -> Optional[Memento]:
        return self._mementos[-1] if self._mementos else None
    
    def count(self) -> int:
        return len(self._mementos)

# Usage
originator = Originator(value=10, name="Initial")
caretaker = Caretaker()

# Sauvegarder état
caretaker.save(originator.create_memento())

# Modifier
originator.value = 20
originator.name = "Modified"

# Sauvegarder nouveau état
caretaker.save(originator.create_memento())

# Restaurer état précédent
originator.restore_from_memento(caretaker.get(0))
print(originator)  # value=10, name="Initial"


[OK] CONCURRENCE & THREADING

from threading import Lock, RLock
from concurrent.futures import ThreadPoolExecutor

# Thread-safe dataclass
@dataclass
class ThreadSafeCounter:
    _value: int = field(default=0, init=False, repr=False)
    _lock: Lock = field(default_factory=Lock, init=False, repr=False)
    
    @property
    def value(self) -> int:
        with self._lock:
            return self._value
    
    def increment(self, amount: int = 1):
        with self._lock:
            self._value += amount
    
    def decrement(self, amount: int = 1):
        with self._lock:
            self._value -= amount
    
    def reset(self):
        with self._lock:
            self._value = 0

# Usage multithread
counter = ThreadSafeCounter()

def worker():
    for _ in range(1000):
        counter.increment()

with ThreadPoolExecutor(max_workers=10) as executor:
    futures = [executor.submit(worker) for _ in range(10)]
    for future in futures:
        future.result()

print(counter.value)  # 10000 (correct avec lock)


# Copy-on-write pour immutabilité thread-safe
@dataclass(frozen=True)
class ImmutableConfig:
    host: str
    port: int
    debug: bool
    _lock: ClassVar[RLock] = RLock()
    _instance: ClassVar[Optional['ImmutableConfig']] = None
    
    @classmethod
    def get_instance(cls) -> 'ImmutableConfig':
        with cls._lock:
            if cls._instance is None:
                cls._instance = cls("localhost", 8080, False)
            return cls._instance
    
    def with_host(self, host: str) -> 'ImmutableConfig':
        return replace(self, host=host)
    
    def with_port(self, port: int) -> 'ImmutableConfig':
        return replace(self, port=port)


# Async dataclass operations
import asyncio

@dataclass
class AsyncService:
    name: str
    delay: float = 1.0
    
    async def fetch_data(self) -> dict:
        """Simule opération async"""
        await asyncio.sleep(self.delay)
        return {"service": self.name, "data": "result"}
    
    async def process(self, data: dict) -> dict:
        """Traite les données de manière asynchrone"""
        await asyncio.sleep(self.delay)
        return {**data, "processed": True}

async def main():
    services = [
        AsyncService("service1", 0.5),
        AsyncService("service2", 0.3),
        AsyncService("service3", 0.7),
    ]
    
    # Exécuter en parallèle
    results = await asyncio.gather(
        *[service.fetch_data() for service in services]
    )
    
    print(results)

# asyncio.run(main())


[OK] CACHING & MEMOIZATION

from functools import lru_cache, cached_property

# Cached property sur dataclass
@dataclass
class ExpensiveComputation:
    data: List[int]
    
    @cached_property
    def sum_value(self) -> int:
        """Calculé une seule fois puis mis en cache"""
        print("Computing sum...")
        return sum(self.data)
    
    @cached_property
    def average(self) -> float:
        """Calculé une seule fois puis mis en cache"""
        print("Computing average...")
        return self.sum_value / len(self.data) if self.data else 0

obj = ExpensiveComputation([1, 2, 3, 4, 5])
print(obj.sum_value)  # Affiche "Computing sum..." puis 15
print(obj.sum_value)  # 15 (pas de recalcul)
print(obj.average)    # Affiche "Computing average..." puis 3.0


# Memoization manuelle
@dataclass
class MemoizedCalculator:
    _cache: dict = field(default_factory=dict, init=False, repr=False)
    
    def fibonacci(self, n: int) -> int:
        if n in self._cache:
            return self._cache[n]
        
        if n <= 1:
            result = n
        else:
            result = self.fibonacci(n-1) + self.fibonacci(n-2)
        
        self._cache[n] = result
        return result
    
    def clear_cache(self):
        self._cache.clear()


# LRU Cache pour méthodes
@dataclass
class DataProcessor:
    max_cache_size: int = 128
    
    def process(self, data: str) -> str:
        """Méthode avec cache LRU"""
        return self._process_cached(data)
    
    @lru_cache(maxsize=128)
    def _process_cached(self, data: str) -> str:
        # Traitement coûteux
        return data.upper()


# Cache avec TTL (Time To Live)
from time import time

@dataclass
class CacheEntry:
    value: Any
    expires_at: float

@dataclass
class TTLCache:
    ttl_seconds: float = 60.0
    _cache: dict = field(default_factory=dict, init=False, repr=False)
    
    def set(self, key: str, value: Any):
        expires_at = time() + self.ttl_seconds
        self._cache[key] = CacheEntry(value, expires_at)
    
    def get(self, key: str) -> Optional[Any]:
        if key not in self._cache:
            return None
        
        entry = self._cache[key]
        if time() > entry.expires_at:
            del self._cache[key]
            return None
        
        return entry.value
    
    def clear_expired(self):
        current_time = time()
        expired_keys = [
            key for key, entry in self._cache.items()
            if current_time > entry.expires_at
        ]
        for key in expired_keys:
            del self._cache[key]


