# Fichier: python_cheats/cheatsheets/poo_ultra_detaille.txt
# GUIDE COMPLET: PROGRAMMATION ORIENTÉE OBJET EN PYTHON
# Pour étudiants en Génie Logiciel - Niveau Débutant à Avancé

"""
═══════════════════════════════════════════════════════════════
  [DOCS] TABLE DES MATIÈRES
═══════════════════════════════════════════════════════════════

PARTIE 1: CLASSES ET OBJETS - FONDAMENTAUX
    [OK] Introduction à la POO
    [OK] Classes et objets de base
    [OK] Méthodes et attributs
    [OK] Constructeur __init__
    [OK] self - Comprendre son rôle
    [OK] Attributs de classe vs d'instance
    [OK] Méthodes de classe et statiques

PARTIE 2: ENCAPSULATION ET PROPRIÉTÉS
    [OK] Encapsulation en Python
    [OK] Convention _ et __
    [OK] Properties (@property)
    [OK] Getters et setters pythoniques
    [OK] Validation et contrôle d'accès

PARTIE 3: HÉRITAGE
    [OK] Héritage simple
    [OK] super() et son utilisation
    [OK] Héritage multiple
    [OK] MRO (Method Resolution Order)
    [OK] Mixins

PARTIE 4: POLYMORPHISME ET MÉTHODES SPÉCIALES
    [OK] Duck typing
    [OK] Méthodes magiques (__str__, __repr__, etc.)
    [OK] Surcharge d'opérateurs
    [OK] Protocols et ABC

PARTIE 5: CLASSES ABSTRAITES ET INTERFACES
    [OK] ABC (Abstract Base Classes)
    [OK] @abstractmethod
    [OK] Protocols (Python 3.8+)
    [OK] Interfaces pythoniques

PARTIE 6: COMPOSITION ET AGRÉGATION
    [OK] Composition vs Héritage
    [OK] Agrégation
    [OK] Délégation
    [OK] Design patterns

PARTIE 7: CONCEPTS AVANCÉS
    [OK] Dataclasses
    [OK] Descriptors
    [OK] Métaclasses
    [OK] Décorateurs de classe
    [OK] Context managers

═══════════════════════════════════════════════════════════════
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] PARTIE 1: CLASSES ET OBJETS - FONDAMENTAUX
═══════════════════════════════════════════════════════════════
"""

"""
QU'EST-CE QUE LA POO?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

La Programmation Orientée Objet est un paradigme qui organise le code
autour d'OBJETS qui combinent:
    - Données (attributs)
    - Comportements (méthodes)

POURQUOI LA POO?
[OK] Code réutilisable et modulaire
[OK] Modélisation proche du monde réel
[OK] Maintenance facilitée
[OK] Organisation logique
[OK] Abstraction et encapsulation

4 PILIERS DE LA POO:
1. ENCAPSULATION: Regrouper données et méthodes, cacher détails
2. HÉRITAGE: Réutiliser et étendre le code
3. POLYMORPHISME: Plusieurs formes d'un même concept
4. ABSTRACTION: Se concentrer sur l'essentiel

CONCEPTS CLÉS:
- Classe: Modèle/Blueprint pour créer des objets
- Objet: Instance concrète d'une classe
- Attribut: Variable appartenant à un objet
- Méthode: Fonction appartenant à un objet
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] CLASSE - LE BLUEPRINT
═══════════════════════════════════════════════════════════════
"""

"""
SYNTAXE DE BASE:
    class NomClasse:
        '''Documentation de la classe'''
        # Attributs et méthodes

CONVENTIONS DE NOMMAGE (PEP 8):
[OK] Classe: PascalCase (Student, BankAccount)
[OK] Attribut/Méthode: snake_case (first_name, calculate_total)
[OK] Constante: UPPER_SNAKE_CASE (MAX_VALUE, PI)
[OK] Privé: _nom ou __nom (convention, pas strict)

PYTHON vs JAVA:
- Pas de modificateurs d'accès (public/private)
- Tout est "public" par convention
- _ signale "interne" (convention)
- __ déclenche name mangling
"""

# ═══ CLASSE SIMPLE ═══

class Person:
    """
    Représente une personne avec nom, âge et email.
    
    C'est la forme la plus basique d'une classe.
    """
    pass  # Classe vide pour l'instant


# Créer des objets (instances)
print("=== CRÉATION D'OBJETS ===")

person1 = Person()
person2 = Person()

print(f"person1 est un objet de type: {type(person1)}")
print(f"person1 ID: {id(person1)}")
print(f"person2 ID: {id(person2)}")
print(f"Sont-ils différents? {person1 is not person2}")

"""
IMPORTANT:
- person1 et person2 sont des objets DIFFÉRENTS
- Chaque objet a son propre ID en mémoire
- 'is' compare les identités, '==' compare les valeurs
"""

# ═══ CLASSE AVEC ATTRIBUTS ═══

class PersonAvecAttributs:
    """Personne avec attributs dynamiques."""
    pass


# Ajouter des attributs dynamiquement (possible mais pas recommandé)
person = PersonAvecAttributs()
person.name = "Alice"
person.age = 25
person.email = "alice@email.com"

print("\n=== ATTRIBUTS DYNAMIQUES ===")
print(f"Nom: {person.name}")
print(f"Âge: {person.age}")
print(f"Email: {person.email}")

"""
PROBLÈME AVEC CETTE APPROCHE:
[X] Pas de structure définie
[X] Erreurs faciles (typos)
[X] Pas de validation
[X] Difficile à maintenir

SOLUTION: Utiliser __init__ (constructeur)
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] CONSTRUCTEUR __init__ - INITIALISATION
═══════════════════════════════════════════════════════════════
"""

"""
__init__ EN PYTHON:
- Méthode spéciale appelée automatiquement lors de la création
- Équivalent du constructeur en Java
- Premier paramètre toujours 'self'
- Initialise les attributs de l'objet

SYNTAXE:
    def __init__(self, paramètres):
        self.attribut = valeur

POURQUOI __init__?
[OK] Garantir initialisation correcte
[OK] Valider les données dès la création
[OK] Éviter objets dans état invalide
[OK] Documentation claire des paramètres requis
"""

class Student:
    """
    Représente un étudiant avec nom, âge et matricule.
    
    Attributes:
        name (str): Nom de l'étudiant
        age (int): Âge de l'étudiant
        matricule (str): Numéro de matricule
        moyenne (float): Moyenne générale (défaut: 0.0)
    """
    
    def __init__(self, name, age, matricule):
        """
        Initialise un nouvel étudiant.
        
        Args:
            name (str): Nom de l'étudiant
            age (int): Âge de l'étudiant
            matricule (str): Numéro de matricule
        """
        # Attributs d'instance
        self.name = name
        self.age = age
        self.matricule = matricule
        self.moyenne = 0.0  # Valeur par défaut
        
        print(f"[OK] Étudiant {name} créé")
    
    def afficher(self):
        """Affiche les informations de l'étudiant."""
        print(f"\n{'='*40}")
        print(f"Nom: {self.name}")
        print(f"Âge: {self.age}")
        print(f"Matricule: {self.matricule}")
        print(f"Moyenne: {self.moyenne}")
        print(f"{'='*40}")


# Utilisation
print("\n=== CONSTRUCTEUR __init__ ===\n")

etudiant1 = Student("Alice", 20, "2024001")
etudiant2 = Student("Bob", 22, "2024002")

etudiant1.afficher()
etudiant2.afficher()

# Modifier un attribut
etudiant1.moyenne = 15.5
print("\nAprès modification:")
etudiant1.afficher()

"""
DÉCORTIQUONS __init__:
1. 'self': Référence à l'objet lui-même (comme 'this' en Java)
2. 'name, age, matricule': Paramètres du constructeur
3. 'self.name = name': Crée attribut d'instance
4. Appelé automatiquement avec Student(...)

SELF:
- Premier paramètre de TOUTE méthode d'instance
- Python le passe automatiquement
- Permet d'accéder aux attributs et méthodes de l'objet
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] MÉTHODES - LES COMPORTEMENTS
═══════════════════════════════════════════════════════════════
"""

"""
MÉTHODES EN PYTHON:
- Fonctions définies dans une classe
- Premier paramètre toujours 'self' (pour méthodes d'instance)
- Accèdent aux attributs via self.attribut
- Définissent les comportements de l'objet

TYPES DE MÉTHODES:
1. Méthodes d'instance: Travaillent avec l'instance (self)
2. Méthodes de classe: Travaillent avec la classe (@classmethod)
3. Méthodes statiques: Indépendantes (@staticmethod)
"""

class Rectangle:
    """
    Représente un rectangle avec longueur et largeur.
    
    Démontre différents types de méthodes.
    """
    
    def __init__(self, longueur, largeur):
        """Initialise le rectangle."""
        self.longueur = longueur
        self.largeur = largeur
    
    # Méthode d'instance - calcule et retourne
    def calculer_aire(self):
        """
        Calcule l'aire du rectangle.
        
        Returns:
            float: Aire du rectangle
        """
        return self.longueur * self.largeur
    
    # Méthode d'instance - calcule et retourne
    def calculer_perimetre(self):
        """Calcule le périmètre du rectangle."""
        return 2 * (self.longueur + self.largeur)
    
    # Méthode d'instance - effectue une action
    def afficher(self):
        """Affiche les dimensions du rectangle."""
        print(f"Rectangle: {self.longueur} x {self.largeur}")
        print(f"Aire: {self.calculer_aire()}")
        print(f"Périmètre: {self.calculer_perimetre()}")
    
    # Méthode d'instance - modifie l'objet
    def redimensionner(self, nouvelle_longueur, nouvelle_largeur):
        """
        Modifie les dimensions du rectangle.
        
        Args:
            nouvelle_longueur (float): Nouvelle longueur
            nouvelle_largeur (float): Nouvelle largeur
        """
        self.longueur = nouvelle_longueur
        self.largeur = nouvelle_largeur
        print("[OK] Rectangle redimensionné!")
    
    # Méthode avec logique conditionnelle
    def est_carre(self):
        """
        Vérifie si le rectangle est un carré.
        
        Returns:
            bool: True si carré, False sinon
        """
        return self.longueur == self.largeur
    
    # Méthode retournant une string
    def get_type(self):
        """Retourne le type de rectangle."""
        if self.longueur == self.largeur:
            return "Carré"
        elif self.longueur > self.largeur:
            return "Rectangle horizontal"
        else:
            return "Rectangle vertical"


# Utilisation des méthodes
print("\n=== MÉTHODES EN ACTION ===\n")

rect = Rectangle(10.0, 5.0)
rect.afficher()

print(f"\nC'est un carré? {rect.est_carre()}")
print(f"Type: {rect.get_type()}")

# Redimensionner
print("\n--- Redimensionnement ---")
rect.redimensionner(8.0, 8.0)
rect.afficher()
print(f"Maintenant c'est un carré? {rect.est_carre()}")

"""
BONNES PRATIQUES MÉTHODES:
[OK] Noms descriptifs en snake_case
[OK] Une méthode = une responsabilité
[OK] Documentation avec docstrings
[OK] Type hints pour clarté (optionnel)
[OK] Retourner des valeurs plutôt que print (sauf affichage)
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] SELF - COMPRENDRE SON RÔLE CRUCIAL
═══════════════════════════════════════════════════════════════
"""

"""
SELF EN PYTHON:
- Référence à l'instance actuelle de l'objet
- TOUJOURS premier paramètre des méthodes d'instance
- Permet d'accéder aux attributs et méthodes de l'objet
- Nom 'self' est une CONVENTION (peut être autre, mais ne le faites pas!)

ÉQUIVALENCE:
Python:  self
Java:    this
C++:     this

POURQUOI 'self'?
[OK] Explicite > Implicite (philosophie Python)
[OK] Distingue variables locales et attributs
[OK] Permet introspection et manipulation avancée
"""

class BankAccount:
    """
    Compte bancaire démontrant l'utilisation de self.
    """
    
    def __init__(self, account_number, owner, balance=0.0):
        """
        Initialise un compte bancaire.
        
        Args:
            account_number (str): Numéro de compte
            owner (str): Propriétaire du compte
            balance (float): Solde initial (défaut: 0.0)
        """
        # self distingue attributs et paramètres
        self.account_number = account_number  # attribut = paramètre
        self.owner = owner
        self.balance = balance
        
        print(f"[OK] Compte {account_number} créé pour {owner}")
    
    def deposer(self, montant):
        """Dépose de l'argent sur le compte."""
        # self.balance fait référence à l'attribut de l'instance
        self.balance += montant
        print(f"[OK] Dépôt de {montant} FCFA")
        
        # Appel d'une autre méthode avec self
        self.afficher_balance()
    
    def retirer(self, montant):
        """Retire de l'argent du compte."""
        if self.balance >= montant:
            self.balance -= montant
            print(f"[OK] Retrait de {montant} FCFA")
            self.afficher_balance()
            return True
        else:
            print("[X] Solde insuffisant")
            return False
    
    def afficher_balance(self):
        """Affiche le solde actuel."""
        # self.balance et self.owner = attributs de l'instance
        print(f"   Solde de {self.owner}: {self.balance} FCFA")
    
    def transferer(self, compte_destinataire, montant):
        """
        Transfère de l'argent vers un autre compte.
        
        Args:
            compte_destinataire (BankAccount): Compte destination
            montant (float): Montant à transférer
        """
        if self.retirer(montant):  # self = compte source
            compte_destinataire.deposer(montant)  # autre objet
            print(f"[OK] Transfert réussi vers {compte_destinataire.account_number}")
            return True
        return False


# Démonstration de self
print("\n=== SELF EN ACTION ===\n")

compte1 = BankAccount("ACC001", "Alice", 50000)
compte2 = BankAccount("ACC002", "Bob", 30000)

print("\n--- Opérations ---")
compte1.deposer(10000)
compte1.retirer(5000)

print("\n--- Transfert ---")
compte1.transferer(compte2, 15000)

print("\n--- Soldes finaux ---")
compte1.afficher_balance()
compte2.afficher_balance()

"""
COMPRENDRE SELF:

Quand on écrit:
    compte1.deposer(10000)

Python transforme en:
    BankAccount.deposer(compte1, 10000)

'compte1' devient 'self' dans la méthode!

DANS deposer(self, montant):
- self = compte1 (l'objet qui a appelé)
- self.balance = compte1.balance
- self.afficher_balance() = compte1.afficher_balance()

C'est pourquoi:
[OK] self permet d'accéder aux attributs de L'OBJET ACTUEL
[OK] Chaque objet a ses propres valeurs
[OK] self.balance de compte1 ≠ self.balance de compte2
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] VALEURS PAR DÉFAUT ET PARAMÈTRES FLEXIBLES
═══════════════════════════════════════════════════════════════
"""

"""
PYTHON vs JAVA - CONSTRUCTEURS:
Java: Surcharge de constructeurs (plusieurs constructeurs)
Python: Un seul __init__ avec paramètres par défaut

AVANTAGES PYTHON:
[OK] Un seul constructeur
[OK] Paramètres optionnels
[OK] Arguments nommés
[OK] *args et **kwargs
"""

class Product:
    """
    Produit avec paramètres par défaut et flexibles.
    """
    
    def __init__(self, nom, prix, quantite=0, categorie="Général"):
        """
        Initialise un produit.
        
        Args:
            nom (str): Nom du produit
            prix (float): Prix unitaire
            quantite (int, optional): Quantité en stock. Défaut: 0
            categorie (str, optional): Catégorie. Défaut: "Général"
        """
        self.nom = nom
        self.prix = prix
        self.quantite = quantite
        self.categorie = categorie
    
    def afficher(self):
        """Affiche les informations du produit."""
        print(f"\nProduit: {self.nom}")
        print(f"Prix: {self.prix} FCFA")
        print(f"Quantité: {self.quantite}")
        print(f"Catégorie: {self.categorie}")


# Différentes façons de créer des produits
print("\n=== PARAMÈTRES PAR DÉFAUT ===\n")

# Tous les paramètres
p1 = Product("Ordinateur", 500000, 10, "Électronique")

# Sans quantité et catégorie (utilisent défauts)
p2 = Product("Souris", 5000)

# Avec arguments nommés (ordre flexible)
p3 = Product(nom="Clavier", categorie="Électronique", prix=15000, quantite=25)

# Mix positionnel et nommé
p4 = Product("Écran", 200000, categorie="Électronique")

p1.afficher()
p2.afficher()
p3.afficher()
p4.afficher()

"""
ARGUMENTS NOMMÉS:
[OK] Plus lisibles
[OK] Ordre flexible
[OK] Évite erreurs
[OK] Documentation auto

RECOMMANDATION:
Pour fonctions avec 3+ paramètres, utilisez arguments nommés
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] ATTRIBUTS DE CLASSE vs ATTRIBUTS D'INSTANCE
═══════════════════════════════════════════════════════════════
"""

"""
ATTRIBUT D'INSTANCE:
- Unique à chaque objet
- Défini dans __init__ avec self.
- Chaque objet a sa propre copie
- Exemple: nom, âge d'une personne

ATTRIBUT DE CLASSE:
- Partagé par TOUTES les instances
- Défini directement dans la classe (pas dans __init__)
- Une seule copie pour toute la classe
- Exemple: compteur d'instances, constantes

SYNTAXE:
    class MaClasse:
        attribut_classe = valeur  # Attribut de classe
        
        def __init__(self):
            self.attribut_instance = valeur  # Attribut d'instance
"""

class Car:
    """
    Voiture démontrant attributs de classe et d'instance.
    """
    
    # ATTRIBUTS DE CLASSE (partagés par toutes les instances)
    nombre_voitures = 0  # Compteur
    TVA = 0.18          # Constante
    marques_populaires = ["Toyota", "Honda", "Ford"]  # Liste partagée
    
    def __init__(self, marque, modele, annee):
        """
        Initialise une voiture.
        
        Args:
            marque (str): Marque de la voiture
            modele (str): Modèle
            annee (int): Année de fabrication
        """
        # ATTRIBUTS D'INSTANCE (uniques à chaque objet)
        self.marque = marque
        self.modele = modele
        self.annee = annee
        
        # Incrémenter le compteur de classe
        Car.nombre_voitures += 1
        print(f"[OK] Voiture créée. Total: {Car.nombre_voitures}")
    
    def afficher(self):
        """Affiche les infos de la voiture."""
        print(f"{self.marque} {self.modele} ({self.annee})")
    
    @classmethod
    def get_nombre_voitures(cls):
        """
        Retourne le nombre total de voitures.
        
        Returns:
            int: Nombre de voitures créées
        """
        return cls.nombre_voitures
    
    @staticmethod
    def calculer_prix_ttc(prix_ht):
        """
        Calcule le prix TTC.
        
        Args:
            prix_ht (float): Prix hors taxes
            
        Returns:
            float: Prix TTC
        """
        return prix_ht * (1 + Car.TVA)


# Démonstration
print("\n=== ATTRIBUTS DE CLASSE vs INSTANCE ===\n")

# Accès attribut de classe SANS créer d'objet
print(f"Voitures au départ: {Car.nombre_voitures}")
print(f"TVA: {Car.TVA}")
print(f"Marques populaires: {Car.marques_populaires}")

print("\n--- Création de voitures ---")
voiture1 = Car("Toyota", "Corolla", 2020)
voiture2 = Car("Honda", "Civic", 2021)
voiture3 = Car("Ford", "Focus", 2019)

# Le compteur est partagé
print(f"\nNombre total: {Car.nombre_voitures}")
print(f"Via méthode de classe: {Car.get_nombre_voitures()}")

# Calcul avec méthode statique
print(f"\n--- Prix TTC ---")
prix_ht = 10000000
prix_ttc = Car.calculer_prix_ttc(prix_ht)
print(f"Prix HT: {prix_ht} FCFA")
print(f"Prix TTC: {prix_ttc} FCFA")

# Chaque voiture a ses propres attributs d'instance
print("\n--- Attributs d'instance ---")
voiture1.afficher()
voiture2.afficher()

"""
ATTENTION - PIÈGE AVEC ATTRIBUTS MUTABLES:
"""

class PiegeClasse:
    """Démontre un piège courant."""
    
    # [X] DANGEREUX: Liste comme attribut de classe
    items = []
    
    def ajouter(self, item):
        self.items.append(item)  # Modifie la liste PARTAGÉE!


obj1 = PiegeClasse()
obj2 = PiegeClasse()

obj1.ajouter("A")
obj2.ajouter("B")

print("\n=== PIÈGE ATTRIBUTS MUTABLES ===")
print(f"obj1.items: {obj1.items}")  # ['A', 'B'] - Partagé!
print(f"obj2.items: {obj2.items}")  # ['A', 'B'] - Même liste!

"""
SOLUTION: Utiliser attribut d'instance
"""

class ClasseCorrecte:
    """Version corrigée."""
    
    def __init__(self):
        # [OK] BON: Chaque instance a sa propre liste
        self.items = []
    
    def ajouter(self, item):
        self.items.append(item)


obj3 = ClasseCorrecte()
obj4 = ClasseCorrecte()

obj3.ajouter("A")
obj4.ajouter("B")

print("\n=== VERSION CORRIGÉE ===")
print(f"obj3.items: {obj3.items}")  # ['A']
print(f"obj4.items: {obj4.items}")  # ['B']

"""
RÈGLE D'OR:
[OK] Attributs de classe: Constantes, compteurs, configuration
[X] Attributs de classe: Listes, dicts mutables (sauf besoin spécifique)
[OK] Attributs d'instance: Tout ce qui est unique à l'objet
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] MÉTHODES DE CLASSE ET MÉTHODES STATIQUES
═══════════════════════════════════════════════════════════════
"""

"""
3 TYPES DE MÉTHODES EN PYTHON:

1. MÉTHODE D'INSTANCE:
   - Premier paramètre: self
   - Accède à l'instance et la classe
   - Usage: Opérations sur l'objet
   
2. MÉTHODE DE CLASSE (@classmethod):
   - Premier paramètre: cls (la classe)
   - Accède à la classe, pas l'instance
   - Usage: Factory methods, opérations sur classe
   
3. MÉTHODE STATIQUE (@staticmethod):
   - Pas de premier paramètre spécial
   - N'accède ni à l'instance ni à la classe
   - Usage: Fonctions utilitaires liées logiquement

COMPARAISON:
┌─────────────────┬───────────┬────────────┬─────────────┐
│ Type            │ Paramètre │ Accès      │ Usage       │
├─────────────────┼───────────┼────────────┼─────────────┤
│ Instance        │ self      │ Instance   │ Courant     │
│ Classe          │ cls       │ Classe     │ Factory     │
│ Statique        │ -         │ Aucun      │ Utilitaire  │
└─────────────────┴───────────┴────────────┴─────────────┘
"""

from datetime import datetime

class Employee:
    """
    Employé démontrant les 3 types de méthodes.
    """
    
    # Attribut de classe
    nombre_employes = 0
    taux_augmentation = 0.05
    
    def __init__(self, nom, salaire):
        """Initialise un employé."""
        self.nom = nom
        self.salaire = salaire
        self.date_embauche = datetime.now()
        
        Employee.nombre_employes += 1
    
    # MÉTHODE D'INSTANCE
    def afficher(self):
        """Affiche les infos de l'employé."""
        print(f"\nEmployé: {self.nom}")
        print(f"Salaire: {self.salaire} FCFA")
        print(f"Embauché le: {self.date_embauche.strftime('%Y-%m-%d')}")
    
    # MÉTHODE D'INSTANCE
    def augmenter_salaire(self, pourcentage=None):
        """
        Augmente le salaire.
        
        Args:
            pourcentage (float, optional): Pourcentage d'augmentation.
                Si None, utilise taux_augmentation de la classe.
        """
        if pourcentage is None:
            pourcentage = Employee.taux_augmentation
        
        ancien_salaire = self.salaire
        self.salaire *= (1 + pourcentage)
        print(f"[OK] Salaire de {self.nom}: {ancien_salaire} -> {self.salaire}")
    
    # MÉTHODE DE CLASSE
    @classmethod
    def from_string(cls, emp_str):
        """
        Factory method: Crée un employé depuis une string.
        
        Args:
            emp_str (str): String format "Nom-Salaire"
            
        Returns:
            Employee: Nouvelle instance
            
        Exemple:
            >>> emp = Employee.from_string("Alice-500000")
        """
        nom, salaire = emp_str.split('-')
        return cls(nom, float(salaire))
    
    # MÉTHODE DE CLASSE
    @classmethod
    def get_nombre_employes(cls):
        """Retourne le nombre d'employés."""
        return cls.nombre_employes
    
    # MÉTHODE DE CLASSE
    @classmethod
    def set_taux_augmentation(cls, nouveau_taux):
        """
        Modifie le taux d'augmentation pour tous.
        
        Args:
            nouveau_taux (float): Nouveau taux (ex: 0.10 pour 10%)
        """
        cls.taux_augmentation = nouveau_taux
        print(f"[OK] Taux d'augmentation fixé à {nouveau_taux*100}%")
    
    # MÉTHODE STATIQUE
    @staticmethod
    def est_salaire_valide(salaire):
        """
        Vérifie si un salaire est valide.
        
        Args:
            salaire (float): Salaire à vérifier
            
        Returns:
            bool: True si valide
        """
        SALAIRE_MIN = 50000
        SALAIRE_MAX = 10000000
        return SALAIRE_MIN <= salaire <= SALAIRE_MAX
    
    # MÉTHODE STATIQUE
    @staticmethod
    def calculer_impot(salaire):
        """
        Calcule l'impôt sur le salaire.
        
        Args:
            salaire (float): Salaire brut
            
        Returns:
            float: Montant de l'impôt
        """
        if salaire < 100000:
            return salaire * 0.10
        elif salaire < 500000:
            return salaire * 0.15
        else:
            return salaire * 0.20


# Démonstration des 3 types de méthodes
print("\n=== TYPES DE MÉTHODES ===\n")

# Méthode de classe - SANS créer d'instance
print(f"Employés au départ: {Employee.get_nombre_employes()}")

# Créer employés normalement
emp1 = Employee("Alice", 400000)

# Factory method (méthode de classe)
emp2 = Employee.from_string("Bob-500000")
emp3 = Employee.from_string("Charlie-350000")

print(f"\nNombre d'employés: {Employee.get_nombre_employes()}")

# Méthodes d'instance
emp1.afficher()
emp2.afficher()

# Méthode statique - SANS instance
print("\n--- Méthodes statiques ---")
print(f"Salaire 300000 valide? {Employee.est_salaire_valide(300000)}")
print(f"Salaire 30000 valide? {Employee.est_salaire_valide(30000)}")
print(f"Impôt sur 400000: {Employee.calculer_impot(400000)} FCFA")

# Modifier taux via méthode de classe
print("\n--- Modification taux ---")
Employee.set_taux_augmentation(0.10)

# Augmentations
print("\n--- Augmentations ---")
emp1.augmenter_salaire()  # Utilise taux de classe (10%)
emp2.augmenter_salaire(0.15)  # Taux personnalisé (15%)

"""
QUAND UTILISER CHAQUE TYPE?

MÉTHODE D'INSTANCE (self):
[OK] Opérations sur l'objet spécifique
[OK] Accès/modification des attributs d'instance
[OK] Comportement qui varie par objet
Exemple: augmenter_salaire(), afficher()

MÉTHODE DE CLASSE (@classmethod):
[OK] Factory methods (constructeurs alternatifs)
[OK] Opérations sur la classe elle-même
[OK] Accès/modification attributs de classe
Exemple: from_string(), set_taux_augmentation()

MÉTHODE STATIQUE (@staticmethod):
[OK] Fonctions utilitaires liées logiquement
[OK] Pas besoin d'accès à classe ou instance
[OK] Pure logique métier
Exemple: est_salaire_valide(), calculer_impot()

RÈGLE SIMPLE:
- Besoin de self? -> Méthode d'instance
- Besoin de cls? -> Méthode de classe
- Besoin de rien? -> Méthode statique
"""

"""
═══════════════════════════════════════════════════════════════
  [GRAPHIQUE] TABLEAU RÉCAPITULATIF - PARTIE 1
═══════════════════════════════════════════════════════════════

┌─────────────────────┬──────────────────────────────────────┐
│ CONCEPT             │ DESCRIPTION                          │
├─────────────────────┼──────────────────────────────────────┤
│ class               │ Définit une classe                   │
│ __init__            │ Constructeur (initialisation)        │
│ self                │ Référence à l'instance actuelle      │
│ Attribut d'instance │ Unique à chaque objet (self.x)       │
│ Attribut de classe  │ Partagé par tous (NomClasse.x)       │
│ Méthode d'instance  │ Premier param: self                  │
│ @classmethod        │ Premier param: cls (classe)          │
│ @staticmethod       │ Pas de param spécial                 │
│ pass                │ Instruction vide (placeholder)       │
└─────────────────────┴──────────────────────────────────────┘

═══════════════════════════════════════════════════════════════
  [OBJECTIF] BONNES PRATIQUES - PARTIE 1
═══════════════════════════════════════════════════════════════

1. NOMMAGE (PEP 8)
   [OK] Classe: PascalCase (Student, BankAccount)
   [OK] Méthode/attribut: snake_case (first_name, get_total)
   [OK] Constante: UPPER_SNAKE_CASE (MAX_VALUE, PI)

2. DOCUMENTATION
   [OK] Docstrings pour classes et méthodes
   [OK] Type hints pour clarté
   [OK] Exemples dans docstrings

3. __init__
   [OK] Toujours définir __init__
   [OK] Initialiser tous les attributs
   [OK] Valider les paramètres

4. ATTRIBUTS
   [OK] Instance: Données uniques à l'objet
   [OK] Classe: Constantes, configuration, compteurs
   [OK] Attention aux mutables en attributs de classe

5. MÉTHODES
   [OK] Une méthode = une responsabilité
   [OK] Noms descriptifs et verbes
   [OK] Retourner plutôt que print

═══════════════════════════════════════════════════════════════
  [ATTENTION] ERREURS COURANTES - PARTIE 1
═══════════════════════════════════════════════════════════════

1. OUBLIER self
   [X] def methode():  # Erreur!
   [OK] def methode(self):

2. CONFUSION ATTRIBUTS
   [X] name = "Alice"  # Attribut de classe!
   [OK] self.name = "Alice"  # Attribut d'instance

3. MUTABLES EN ATTRIBUTS DE CLASSE
   [X] class C:
          items = []  # Partagé par tous!
   [OK] class C:
          def __init__(self):
              self.items = []  # Propre à chaque instance

4. APPEL MÉTHODE
   [X] obj.methode  # Oubli des ()
   [OK] obj.methode()

5. MODIFICATION DIRECTE
   [X] obj.attribut = valeur  # Pas de validation
   [OK] obj.set_attribut(valeur)  # Avec validation

FIN DE LA PARTIE 1
═══════════════════════════════════════════════════════════════

PROCHAINE PARTIE: Encapsulation et Propriétés
- Conventions _ et __
- @property et setters
- Validation et contrôle d'accès
- Propriétés calculées
"""

# ═══════════════════════════════════════════════════════════════
# FIN PARTIE 1
# ═══════════════════════════════════════════════════════════════

"""
═══════════════════════════════════════════════════════════════
  [OK] PARTIE 2: ENCAPSULATION ET PROPRIÉTÉS
═══════════════════════════════════════════════════════════════
"""

"""
QU'EST-CE QUE L'ENCAPSULATION?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

L'encapsulation est le principe de regrouper données et méthodes,
tout en cachant les détails d'implémentation.

PYTHON vs JAVA:
- Java: Modificateurs stricts (private, protected, public)
- Python: Convention + name mangling
- Python: "We're all consenting adults here"

POURQUOI?
[OK] Protéger l'intégrité des données
[OK] Contrôler l'accès et les modifications
[OK] Permettre changements internes sans casser l'API
[OK] Valider les valeurs
[OK] Propriétés calculées

CONVENTIONS PYTHON:
- public:      nom (accessible partout)
- protected:   _nom (convention: usage interne)
- private:     __nom (name mangling)
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] CONVENTIONS _ ET __ (UNDERSCORE)
═══════════════════════════════════════════════════════════════
"""

"""
3 CONVENTIONS EN PYTHON:

1. PUBLIC (aucun underscore):
   nom
   - Partie de l'API publique
   - Utilisable partout
   
2. PROTECTED (un underscore):
   _nom
   - Convention: usage interne
   - Pas de restriction technique
   - "Ne touchez pas sauf si vous savez ce que vous faites"
   
3. PRIVATE (double underscore):
   __nom
   - Name mangling: devient _NomClasse__nom
   - Évite collisions dans héritage
   - Vraiment "caché" (mais pas impossible d'accéder)

IMPORTANT:
Python ne FORCE pas l'encapsulation. C'est une question de confiance
et de conventions. "We're all consenting adults here."
"""

class BankAccountConventions:
    """
    Compte bancaire démontrant les conventions d'encapsulation.
    """
    
    def __init__(self, account_number, owner, balance):
        # PUBLIC - Accessible partout
        self.account_number = account_number
        
        # PROTECTED - Usage interne (convention)
        self._owner = owner
        
        # PRIVATE - Name mangling
        self.__balance = balance
        
        # Attribut vraiment privé pour tracking
        self.__transaction_count = 0
    
    def get_balance(self):
        """Retourne le solde (méthode publique)."""
        return self.__balance
    
    def deposit(self, amount):
        """Dépose de l'argent."""
        if amount > 0:
            self.__balance += amount
            self.__transaction_count += 1
            print(f"[OK] Dépôt de {amount}")
            return True
        return False
    
    def withdraw(self, amount):
        """Retire de l'argent."""
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            self.__transaction_count += 1
            print(f"[OK] Retrait de {amount}")
            return True
        print("[X] Opération impossible")
        return False
    
    def _internal_audit(self):
        """Méthode protected (usage interne)."""
        print(f"[AUDIT] Compte: {self.account_number}")
        print(f"[AUDIT] Transactions: {self.__transaction_count}")
    
    def __validate_amount(self, amount):
        """Méthode private (name mangling)."""
        return isinstance(amount, (int, float)) and amount > 0


# Démonstration
print("\n=== CONVENTIONS ENCAPSULATION ===\n")

account = BankAccountConventions("ACC001", "Alice", 10000)

# PUBLIC - OK
print(f"Numéro (public): {account.account_number}")

# PROTECTED - Accessible mais déconseillé
print(f"Owner (protected): {account._owner}")  # Fonctionne mais bad practice

# PRIVATE - Name mangling
# print(account.__balance)  # [X] AttributeError!

# Accès correct via méthode publique
print(f"Balance (via getter): {account.get_balance()}")

# On PEUT accéder au private avec name mangling (mais ne le faites pas!)
print(f"Balance (mangled): {account._BankAccountConventions__balance}")

account.deposit(5000)
account.withdraw(3000)

# Méthode protected - accessible mais déconseillée
account._internal_audit()

"""
RÈGLES D'OR:

[OK] PUBLIC (nom):
  - API officielle
  - Stable et documentée
  - Utilisez librement

[OK] PROTECTED (_nom):
  - Implémentation interne
  - Peut changer
  - N'utilisez que si vous maintenez le code
  
[OK] PRIVATE (__nom):
  - Vraiment interne
  - Évite collisions dans héritage
  - N'accédez JAMAIS directement

EN PRATIQUE:
- Commencez par public
- Utilisez _protected pour signaler "interne"
- Utilisez __private rarement (conflits héritage)
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] PROPERTIES (@property) - LA FAÇON PYTHONIQUE
═══════════════════════════════════════════════════════════════
"""

"""
JAVA: getX() / setX()
PYTHON: @property (propriétés)

@property PERMET:
[OK] Accès comme attribut (obj.attr) mais avec contrôle
[OK] Validation automatique
[OK] Propriétés calculées
[OK] Logique cachée derrière interface simple
[OK] Rétrocompatibilité

SYNTAXE:
    @property
    def nom(self):
        '''Getter'''
        return self._nom
    
    @nom.setter
    def nom(self, value):
        '''Setter'''
        # Validation
        self._nom = value
    
    @nom.deleter
    def nom(self):
        '''Deleter (optionnel)'''
        del self._nom

USAGE:
    obj.nom           # Appelle getter
    obj.nom = value   # Appelle setter
    del obj.nom       # Appelle deleter
"""

class Person:
    """
    Personne avec properties pour encapsulation.
    """
    
    def __init__(self, first_name, last_name, age):
        """Initialise une personne."""
        # Utilise les setters via properties
        self.first_name = first_name  # Appelle @first_name.setter
        self.last_name = last_name
        self.age = age
    
    # ═══ PROPERTY: first_name ═══
    
    @property
    def first_name(self):
        """
        Getter pour first_name.
        
        Returns:
            str: Prénom
        """
        return self._first_name
    
    @first_name.setter
    def first_name(self, value):
        """
        Setter pour first_name avec validation.
        
        Args:
            value (str): Nouveau prénom
            
        Raises:
            ValueError: Si prénom invalide
        """
        if not value or not isinstance(value, str):
            raise ValueError("Prénom doit être une chaîne non vide")
        if len(value) < 2:
            raise ValueError("Prénom doit avoir au moins 2 caractères")
        self._first_name = value.strip().capitalize()
    
    # ═══ PROPERTY: last_name ═══
    
    @property
    def last_name(self):
        """Getter pour last_name."""
        return self._last_name
    
    @last_name.setter
    def last_name(self, value):
        """Setter pour last_name avec validation."""
        if not value or not isinstance(value, str):
            raise ValueError("Nom doit être une chaîne non vide")
        self._last_name = value.strip().upper()
    
    # ═══ PROPERTY: age ═══
    
    @property
    def age(self):
        """Getter pour age."""
        return self._age
    
    @age.setter
    def age(self, value):
        """Setter pour age avec validation."""
        if not isinstance(value, int):
            raise TypeError("Âge doit être un entier")
        if not 0 <= value <= 150:
            raise ValueError("Âge doit être entre 0 et 150")
        self._age = value
    
    # ═══ PROPERTY CALCULÉE: full_name ═══
    
    @property
    def full_name(self):
        """
        Nom complet (calculé dynamiquement).
        
        Returns:
            str: Nom complet
        """
        return f"{self._first_name} {self._last_name}"
    
    @full_name.setter
    def full_name(self, value):
        """
        Définit le nom complet en séparant prénom et nom.
        
        Args:
            value (str): Nom complet "Prénom Nom"
        """
        parts = value.split()
        if len(parts) < 2:
            raise ValueError("Format: 'Prénom Nom'")
        self.first_name = parts[0]
        self.last_name = ' '.join(parts[1:])
    
    # ═══ PROPERTY CALCULÉE: is_adult ═══
    
    @property
    def is_adult(self):
        """
        Vérifie si la personne est majeure.
        
        Returns:
            bool: True si >= 18 ans
        """
        return self._age >= 18
    
    # ═══ PROPERTY CALCULÉE: birth_year ═══
    
    @property
    def birth_year(self):
        """
        Calcule l'année de naissance approximative.
        
        Returns:
            int: Année de naissance estimée
        """
        from datetime import datetime
        return datetime.now().year - self._age
    
    def display(self):
        """Affiche les informations."""
        print(f"\n{'='*50}")
        print(f"Nom complet: {self.full_name}")
        print(f"Âge: {self.age} ans")
        print(f"Année de naissance: ~{self.birth_year}")
        print(f"Majeur: {'Oui' if self.is_adult else 'Non'}")
        print(f"{'='*50}")


# Démonstration des properties
print("\n=== PROPERTIES (@property) ===\n")

# Création avec validation automatique
person = Person("alice", "dupont", 25)
person.display()

# Accès comme attributs simples
print(f"\nPrénom: {person.first_name}")  # Appelle getter
print(f"Nom: {person.last_name}")
print(f"Âge: {person.age}")

# Modification avec validation automatique
print("\n--- Modifications ---")
person.first_name = "  bob  "  # Sera nettoyé et capitalisé
person.last_name = "martin"     # Sera mis en majuscules
person.age = 30

print(f"Prénom modifié: {person.first_name}")
print(f"Nom modifié: {person.last_name}")

# Property calculée
print(f"\nNom complet: {person.full_name}")

# Setter de property calculée
person.full_name = "Charlie Bernard"
print(f"Après modification complète: {person.full_name}")

# Properties en lecture seule (calculées)
print(f"\nEst majeur? {person.is_adult}")
print(f"Année de naissance: {person.birth_year}")

# Validation en action
print("\n--- Tests de validation ---")
try:
    person.age = -5
except ValueError as e:
    print(f"[X] Erreur: {e}")

try:
    person.first_name = ""
except ValueError as e:
    print(f"[X] Erreur: {e}")

try:
    person.age = "trente"
except TypeError as e:
    print(f"[X] Erreur: {e}")

"""
AVANTAGES @property:

[OK] SYNTAXE SIMPLE:
  person.age = 25        # Au lieu de person.set_age(25)
  print(person.age)      # Au lieu de person.get_age()

[OK] VALIDATION AUTOMATIQUE:
  Impossible de mettre valeur invalide

[OK] PROPRIÉTÉS CALCULÉES:
  full_name, is_adult se calculent à la demande

[OK] RÉTROCOMPATIBILITÉ:
  Peut transformer attribut en property sans changer code client

[OK] ENCAPSULATION:
  Attribut réel (_age) caché, accès contrôlé
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] VALIDATION ET CONTRÔLE D'ACCÈS AVANCÉS
═══════════════════════════════════════════════════════════════
"""

class Product:
    """
    Produit avec validation avancée et propriétés.
    """
    
    # Constantes de classe
    MIN_PRICE = 0
    MAX_PRICE = 100_000_000
    VALID_CATEGORIES = ["Électronique", "Vêtements", "Alimentaire", "Autre"]
    
    def __init__(self, name, price, quantity=0, category="Autre"):
        """Initialise un produit."""
        self.name = name
        self.price = price
        self.quantity = quantity
        self.category = category
        self._discount_rate = 0.0
    
    # ═══ NAME ═══
    
    @property
    def name(self):
        """Nom du produit."""
        return self._name
    
    @name.setter
    def name(self, value):
        """Valide et définit le nom."""
        if not value or not isinstance(value, str):
            raise ValueError("Nom doit être une chaîne non vide")
        if len(value) < 3:
            raise ValueError("Nom doit avoir au moins 3 caractères")
        self._name = value.strip()
    
    # ═══ PRICE ═══
    
    @property
    def price(self):
        """Prix du produit."""
        return self._price
    
    @price.setter
    def price(self, value):
        """Valide et définit le prix."""
        if not isinstance(value, (int, float)):
            raise TypeError("Prix doit être un nombre")
        if not self.MIN_PRICE <= value <= self.MAX_PRICE:
            raise ValueError(
                f"Prix doit être entre {self.MIN_PRICE} et {self.MAX_PRICE}"
            )
        self._price = float(value)
    
    # ═══ QUANTITY ═══
    
    @property
    def quantity(self):
        """Quantité en stock."""
        return self._quantity
    
    @quantity.setter
    def quantity(self, value):
        """Valide et définit la quantité."""
        if not isinstance(value, int):
            raise TypeError("Quantité doit être un entier")
        if value < 0:
            raise ValueError("Quantité ne peut pas être négative")
        self._quantity = value
    
    # ═══ CATEGORY ═══
    
    @property
    def category(self):
        """Catégorie du produit."""
        return self._category
    
    @category.setter
    def category(self, value):
        """Valide et définit la catégorie."""
        if value not in self.VALID_CATEGORIES:
            raise ValueError(
                f"Catégorie doit être parmi: {', '.join(self.VALID_CATEGORIES)}"
            )
        self._category = value
    
    # ═══ PROPERTIES CALCULÉES ═══
    
    @property
    def is_available(self):
        """Vérifie si le produit est disponible."""
        return self._quantity > 0
    
    @property
    def total_value(self):
        """Valeur totale du stock."""
        return self._price * self._quantity
    
    @property
    def discounted_price(self):
        """Prix après réduction."""
        return self._price * (1 - self._discount_rate)
    
    # ═══ DISCOUNT_RATE (avec validation) ═══
    
    @property
    def discount_rate(self):
        """Taux de réduction."""
        return self._discount_rate
    
    @discount_rate.setter
    def discount_rate(self, value):
        """Définit le taux de réduction."""
        if not isinstance(value, (int, float)):
            raise TypeError("Taux doit être un nombre")
        if not 0 <= value <= 1:
            raise ValueError("Taux doit être entre 0 et 1")
        self._discount_rate = value
    
    # ═══ MÉTHODES ═══
    
    def add_stock(self, amount):
        """Ajoute du stock."""
        if amount > 0:
            self.quantity += amount
            print(f"[OK] {amount} unités ajoutées")
    
    def remove_stock(self, amount):
        """Retire du stock."""
        if amount > self.quantity:
            raise ValueError("Stock insuffisant")
        self.quantity -= amount
        print(f"[OK] {amount} unités retirées")
    
    def apply_discount(self, rate):
        """Applique une réduction."""
        self.discount_rate = rate
        print(f"[OK] Réduction de {rate*100}% appliquée")
    
    def display(self):
        """Affiche les informations."""
        print(f"\n{'='*60}")
        print(f"Produit: {self.name}")
        print(f"Catégorie: {self.category}")
        print(f"Prix: {self.price:,.0f} FCFA")
        if self._discount_rate > 0:
            print(f"Prix réduit: {self.discounted_price:,.0f} FCFA "
                  f"(-{self._discount_rate*100}%)")
        print(f"Quantité: {self.quantity}")
        print(f"Disponible: {'Oui' if self.is_available else 'Non'}")
        print(f"Valeur stock: {self.total_value:,.0f} FCFA")
        print(f"{'='*60}")


# Démonstration validation avancée
print("\n=== VALIDATION AVANCÉE ===\n")

# Création avec validation
product = Product("Ordinateur Portable", 500_000, 10, "Électronique")
product.display()

# Modifications valides
print("\n--- Modifications valides ---")
product.price = 450_000
product.quantity = 15
product.apply_discount(0.10)
product.display()

# Tests de validation
print("\n--- Tests de validation ---")

# Prix invalide
try:
    product.price = -1000
except ValueError as e:
    print(f"[X] Prix négatif: {e}")

# Catégorie invalide
try:
    product.category = "Invalide"
except ValueError as e:
    print(f"[X] Catégorie: {e}")

# Quantité invalide
try:
    product.quantity = -5
except ValueError as e:
    print(f"[X] Quantité: {e}")

# Type invalide
try:
    product.price = "gratuit"
except TypeError as e:
    print(f"[X] Type prix: {e}")

# Réduction invalide
try:
    product.discount_rate = 1.5
except ValueError as e:
    print(f"[X] Réduction: {e}")

"""
PATTERNS DE VALIDATION:

1. TYPE CHECKING:
   isinstance(value, type)

2. RANGE CHECKING:
   MIN <= value <= MAX

3. ENUM/CHOICES:
   value in VALID_VALUES

4. FORMAT CHECKING:
   regex, len(), etc.

5. BUSINESS RULES:
   Logique métier spécifique
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] PROPRIÉTÉS READ-ONLY (LECTURE SEULE)
═══════════════════════════════════════════════════════════════
"""

"""
PROPRIÉTÉS EN LECTURE SEULE:
- Définir seulement le getter
- Pas de setter
- Calculées ou fixées à la création

USAGES:
[OK] ID immuables
[OK] Timestamps
[OK] Propriétés calculées
[OK] Valeurs dérivées
"""

from datetime import datetime
import uuid

class Transaction:
    """
    Transaction avec propriétés read-only.
    """
    
    def __init__(self, sender, receiver, amount, transaction_type="transfer"):
        """Initialise une transaction."""
        # ID unique généré automatiquement
        self._id = str(uuid.uuid4())
        
        # Timestamp de création
        self._timestamp = datetime.now()
        
        # Données de transaction
        self._sender = sender
        self._receiver = receiver
        self._amount = amount
        self._type = transaction_type
        
        # Statut
        self._status = "pending"
    
    # ═══ READ-ONLY PROPERTIES ═══
    
    @property
    def id(self):
        """ID unique de la transaction (read-only)."""
        return self._id
    
    @property
    def timestamp(self):
        """Timestamp de création (read-only)."""
        return self._timestamp
    
    @property
    def sender(self):
        """Expéditeur (read-only après création)."""
        return self._sender
    
    @property
    def receiver(self):
        """Destinataire (read-only après création)."""
        return self._receiver
    
    @property
    def amount(self):
        """Montant (read-only après création)."""
        return self._amount
    
    # ═══ READ-WRITE PROPERTY ═══
    
    @property
    def status(self):
        """Statut de la transaction."""
        return self._status
    
    @status.setter
    def status(self, value):
        """Change le statut (avec validation)."""
        valid_statuses = ["pending", "completed", "failed", "cancelled"]
        if value not in valid_statuses:
            raise ValueError(f"Statut doit être: {', '.join(valid_statuses)}")
        
        # Business rule: ne peut pas revenir à pending
        if self._status != "pending" and value == "pending":
            raise ValueError("Impossible de revenir à 'pending'")
        
        self._status = value
    
    # ═══ CALCULATED PROPERTIES ═══
    
    @property
    def is_completed(self):
        """Vérifie si transaction complétée."""
        return self._status == "completed"
    
    @property
    def age_seconds(self):
        """Âge de la transaction en secondes."""
        return (datetime.now() - self._timestamp).total_seconds()
    
    @property
    def formatted_timestamp(self):
        """Timestamp formaté."""
        return self._timestamp.strftime("%Y-%m-%d %H:%M:%S")
    
    def complete(self):
        """Marque la transaction comme complétée."""
        self.status = "completed"
        print(f"[OK] Transaction {self.id[:8]}... complétée")
    
    def cancel(self):
        """Annule la transaction."""
        if self._status == "completed":
            raise ValueError("Impossible d'annuler une transaction complétée")
        self.status = "cancelled"
        print(f"[OK] Transaction {self.id[:8]}... annulée")
    
    def display(self):
        """Affiche les détails."""
        print(f"\n{'='*60}")
        print(f"ID: {self.id}")
        print(f"Timestamp: {self.formatted_timestamp}")
        print(f"De: {self.sender}")
        print(f"À: {self.receiver}")
        print(f"Montant: {self.amount:,.0f} FCFA")
        print(f"Statut: {self.status}")
        print(f"Âge: {self.age_seconds:.2f}s")
        print(f"{'='*60}")


# Démonstration read-only
print("\n=== PROPRIÉTÉS READ-ONLY ===\n")

trans = Transaction("Alice", "Bob", 50000)
trans.display()

# Lecture OK
print(f"\nID: {trans.id}")
print(f"Sender: {trans.sender}")
print(f"Amount: {trans.amount}")

# Modification du statut OK (a un setter)
print("\n--- Changement de statut ---")
trans.status = "completed"
print(f"Statut: {trans.status}")

# Tentative de modification read-only
print("\n--- Tentatives de modification ---")
try:
    trans.id = "nouveau-id"
except AttributeError as e:
    print(f"[X] Modification ID: can't set attribute")

try:
    trans.amount = 100000
except AttributeError as e:
    print(f"[X] Modification amount: can't set attribute")

# Business rule validation
try:
    trans.status = "pending"
except ValueError as e:
    print(f"[X] Retour à pending: {e}")

"""
CRÉER PROPERTY READ-ONLY:

Méthode 1: Seulement @property (pas de setter)
    @property
    def attr(self):
        return self._attr

Méthode 2: Property avec error dans setter
    @property
    def attr(self):
        return self._attr
    
    @attr.setter
    def attr(self, value):
        raise AttributeError("Attribut read-only")

Méthode 3 (old style): property()
    attr = property(lambda self: self._attr)
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] EXEMPLE COMPLET: SYSTÈME BANCAIRE ENCAPSULÉ
═══════════════════════════════════════════════════════════════
"""

class BankAccountComplete:
    """
    Compte bancaire avec encapsulation complète.
    """
    
    # Constantes de classe
    MIN_BALANCE = 1000
    MAX_WITHDRAWAL = 500_000
    OVERDRAFT_FEE = 5000
    
    def __init__(self, account_number, owner, initial_balance=0):
        """Initialise le compte."""
        self._account_number = account_number
        self._owner = owner
        self._balance = 0
        self._is_active = True
        self._creation_date = datetime.now()
        self._transactions = []
        
        # Dépôt initial
        if initial_balance > 0:
            self._balance = initial_balance
            self._log_transaction("deposit", initial_balance, "Dépôt initial")
    
    # ═══ READ-ONLY PROPERTIES ═══
    
    @property
    def account_number(self):
        """Numéro de compte (immuable)."""
        return self._account_number
    
    @property
    def creation_date(self):
        """Date de création (immuable)."""
        return self._creation_date
    
    @property
    def transaction_history(self):
        """Historique des transactions (copie)."""
        return self._transactions.copy()
    
    # ═══ READ-WRITE PROPERTIES ═══
    
    @property
    def owner(self):
        """Propriétaire du compte."""
        return self._owner
    
    @owner.setter
    def owner(self, value):
        """Change le propriétaire."""
        if not value or len(value) < 3:
            raise ValueError("Nom propriétaire invalide")
        old_owner = self._owner
        self._owner = value
        self._log_transaction("update", 0, f"Changement propriétaire: {old_owner} -> {value}")
    
    @property
    def is_active(self):
        """Statut du compte."""
        return self._is_active
    
    # ═══ CALCULATED PROPERTIES ═══
    
    @property
    def balance(self):
        """Solde actuel (read-only, modifiable via deposit/withdraw)."""
        return self._balance
    
    @property
    def available_balance(self):
        """Solde disponible (tenant compte du minimum)."""
        return max(0, self._balance - self.MIN_BALANCE)
    
    @property
    def account_age_days(self):
        """Âge du compte en jours."""
        return (datetime.now() - self._creation_date).days
    
    @property
    def total_transactions(self):
        """Nombre total de transactions."""
        return len(self._transactions)
    
    # ═══ MÉTHODES PRIVÉES ═══
    
    def _log_transaction(self, trans_type, amount, description=""):
        """Enregistre une transaction (méthode privée)."""
        transaction = {
            "timestamp": datetime.now(),
            "type": trans_type,
            "amount": amount,
            "balance_after": self._balance,
            "description": description
        }
        self._transactions.append(transaction)
    
    def _validate_amount(self, amount):
        """Valide un montant (méthode privée)."""
        if not isinstance(amount, (int, float)):
            raise TypeError("Montant doit être un nombre")
        if amount <= 0:
            raise ValueError("Montant doit être positif")
        return True
    
    def _check_active(self):
        """Vérifie que le compte est actif (méthode privée)."""
        if not self._is_active:
            raise RuntimeError("Compte inactif")
    
    # ═══ MÉTHODES PUBLIQUES ═══
    
    def deposit(self, amount, description="Dépôt"):
        """
        Dépose de l'argent.
        
        Args:
            amount (float): Montant à déposer
            description (str): Description de l'opération
            
        Returns:
            bool: True si succès
        """
        self._check_active()
        self._validate_amount(amount)
        
        self._balance += amount
        self._log_transaction("deposit", amount, description)
        print(f"[OK] Dépôt de {amount:,.0f} FCFA")
        print(f"  Nouveau solde: {self._balance:,.0f} FCFA")
        return True
    
    def withdraw(self, amount, description="Retrait"):
        """
        Retire de l'argent.
        
        Args:
            amount (float): Montant à retirer
            description (str): Description de l'opération
            
        Returns:
            bool: True si succès
        """
        self._check_active()
        self._validate_amount(amount)
        
        # Vérifications
        if amount > self.MAX_WITHDRAWAL:
            raise ValueError(f"Dépassement limite retrait: {self.MAX_WITHDRAWAL:,.0f}")
        
        if self._balance - amount < self.MIN_BALANCE:
            raise ValueError(f"Solde minimum requis: {self.MIN_BALANCE:,.0f}")
        
        self._balance -= amount
        self._log_transaction("withdrawal", amount, description)
        print(f"[OK] Retrait de {amount:,.0f} FCFA")
        print(f"  Nouveau solde: {self._balance:,.0f} FCFA")
        return True
    
    def transfer(self, target_account, amount, description="Transfert"):
        """
        Transfère de l'argent vers un autre compte.
        
        Args:
            target_account (BankAccountComplete): Compte destination
            amount (float): Montant à transférer
            description (str): Description
            
        Returns:
            bool: True si succès
        """
        # Débiter le compte source
        if self.withdraw(amount, f"{description} vers {target_account.account_number}"):
            # Créditer le compte destination
            target_account.deposit(amount, f"{description} de {self.account_number}")
            print(f"[OK] Transfert réussi: {amount:,.0f} FCFA")
            return True
        return False
    
    def close(self):
        """Ferme le compte."""
        if self._balance > 0:
            raise ValueError("Impossible: solde non nul")
        
        self._is_active = False
        self._log_transaction("close", 0, "Fermeture du compte")
        print(f"[OK] Compte {self.account_number} fermé")
    
    def get_statement(self, last_n=10):
        """
        Génère un relevé de compte.
        
        Args:
            last_n (int): Nombre de dernières transactions
        """
        print(f"\n{'='*70}")
        print(f"RELEVÉ DE COMPTE - {self.account_number}")
        print(f"{'='*70}")
        print(f"Propriétaire: {self.owner}")
        print(f"Date création: {self.creation_date.strftime('%Y-%m-%d')}")
        print(f"Âge du compte: {self.account_age_days} jours")
        print(f"Statut: {'Actif' if self.is_active else 'Inactif'}")
        print(f"\nSolde actuel: {self.balance:,.0f} FCFA")
        print(f"Solde disponible: {self.available_balance:,.0f} FCFA")
        print(f"\n{'-'*70}")
        print(f"DERNIÈRES TRANSACTIONS ({min(last_n, len(self._transactions))})")
        print(f"{'-'*70}")
        
        for trans in self._transactions[-last_n:]:
            timestamp = trans["timestamp"].strftime("%Y-%m-%d %H:%M:%S")
            print(f"{timestamp} | {trans['type']:10} | "
                  f"{trans['amount']:>12,.0f} | "
                  f"Solde: {trans['balance_after']:>12,.0f}")
            if trans['description']:
                print(f"  -> {trans['description']}")
        
        print(f"{'='*70}")


# Démonstration système bancaire complet
print("\n=== SYSTÈME BANCAIRE COMPLET ===\n")

# Créer des comptes
compte1 = BankAccountComplete("ACC001", "Alice Dupont", 100_000)
compte2 = BankAccountComplete("ACC002", "Bob Martin", 50_000)

# Opérations
print("\n--- Opérations sur compte1 ---")
compte1.deposit(50_000, "Salaire")
compte1.withdraw(20_000, "Loyer")

print("\n--- Transfert ---")
compte1.transfer(compte2, 30_000, "Remboursement")

# Afficher relevés
compte1.get_statement()
compte2.get_statement()

# Properties
print(f"\n--- Properties ---")
print(f"Solde disponible compte1: {compte1.available_balance:,.0f}")
print(f"Nombre de transactions: {compte1.total_transactions}")
print(f"Âge du compte: {compte1.account_age_days} jours")

# Tests de validation
print("\n--- Tests de validation ---")
try:
    compte1.withdraw(600_000)
except ValueError as e:
    print(f"[X] {e}")

try:
    compte1.withdraw(95_000)
except ValueError as e:
    print(f"[X] {e}")

try:
    compte1.close()
except ValueError as e:
    print(f"[X] {e}")

"""
RÉSUMÉ ENCAPSULATION:

CONVENTIONS:
- public: nom
- protected: _nom (usage interne)
- private: __nom (name mangling)

@property:
- Accès contrôlé comme attributs
- Validation automatique
- Propriétés calculées
- Read-only si pas de setter

VALIDATION:
- Type checking
- Range checking
- Business rules
- Cohérence des données

FIN DE LA PARTIE 2
═══════════════════════════════════════════════════════════════

PROCHAINE PARTIE: Héritage
- Héritage simple
- super() et MRO
- Héritage multiple
- Mixins
"""

# ═══════════════════════════════════════════════════════════════
# FIN PARTIE 2
# ═══════════════════════════════════════════════════════════════

"""
═══════════════════════════════════════════════════════════════
  [OK] PARTIE 3: HÉRITAGE - RÉUTILISATION ET EXTENSION
═══════════════════════════════════════════════════════════════
"""

"""
QU'EST-CE QUE L'HÉRITAGE?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

L'héritage permet à une classe (enfant/sous-classe) d'hériter
des attributs et méthodes d'une autre classe (parent/super-classe).

SYNTAXE:
    class Parent:
        pass
    
    class Enfant(Parent):
        pass

POURQUOI?
[OK] Réutilisation de code
[OK] Organisation hiérarchique
[OK] Extension de fonctionnalités
[OK] Polymorphisme
[OK] Modélisation naturelle du monde réel

RELATION "EST-UN" (IS-A):
- Chien EST-UN Animal
- Manager EST-UN Employé
- VoitureElectrique EST-UNE Voiture

TYPES D'HÉRITAGE EN PYTHON:
1. Héritage simple (une seule classe parent)
2. Héritage multiple (plusieurs classes parents)
3. Héritage multiniveau (hiérarchie)
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] HÉRITAGE SIMPLE
═══════════════════════════════════════════════════════════════
"""

"""
HÉRITAGE SIMPLE:
- Une classe enfant hérite d'UNE classe parent
- Accès à tous les attributs/méthodes publics et protégés
- Peut redéfinir (override) les méthodes
- Peut ajouter nouvelles méthodes/attributs
"""

# ═══ EXEMPLE BASIQUE ═══

class Animal:
    """
    Classe parent représentant un animal générique.
    """
    
    def __init__(self, name, age):
        """Initialise un animal."""
        self.name = name
        self.age = age
        print(f"[Animal.__init__] Animal {name} créé")
    
    def eat(self):
        """L'animal mange."""
        print(f"{self.name} mange")
    
    def sleep(self):
        """L'animal dort."""
        print(f"{self.name} dort")
    
    def make_sound(self):
        """Son générique."""
        print(f"{self.name} fait un son")
    
    def display(self):
        """Affiche les infos."""
        print(f"\nAnimal: {self.name}")
        print(f"Âge: {self.age} ans")


class Dog(Animal):
    """
    Classe enfant héritant d'Animal.
    
    Hérite de: name, age, eat(), sleep(), make_sound(), display()
    """
    
    def __init__(self, name, age, breed):
        """Initialise un chien."""
        # Appeler le constructeur parent
        super().__init__(name, age)
        
        # Attribut spécifique au chien
        self.breed = breed
        print(f"[Dog.__init__] Chien de race {breed} créé")
    
    # Redéfinition (override) de make_sound
    def make_sound(self):
        """Le chien aboie."""
        print(f"{self.name} aboie: Woof! Woof!")
    
    # Nouvelle méthode spécifique
    def fetch(self):
        """Le chien rapporte."""
        print(f"{self.name} rapporte la balle")
    
    # Redéfinition de display pour ajouter breed
    def display(self):
        """Affiche les infos du chien."""
        super().display()  # Appelle la version parent
        print(f"Race: {self.breed}")


class Cat(Animal):
    """Classe chat héritant d'Animal."""
    
    def __init__(self, name, age, color):
        """Initialise un chat."""
        super().__init__(name, age)
        self.color = color
        print(f"[Cat.__init__] Chat de couleur {color} créé")
    
    def make_sound(self):
        """Le chat miaule."""
        print(f"{self.name} miaule: Meow!")
    
    def scratch(self):
        """Le chat griffe."""
        print(f"{self.name} griffe le canapé")
    
    def display(self):
        """Affiche les infos du chat."""
        super().display()
        print(f"Couleur: {self.color}")


# Démonstration héritage simple
print("\n=== HÉRITAGE SIMPLE ===\n")

# Créer instances
print("--- Création ---")
dog = Dog("Rex", 3, "Labrador")
cat = Cat("Minou", 2, "Gris")

print("\n--- Méthodes héritées ---")
dog.eat()    # Hérité d'Animal
dog.sleep()  # Hérité d'Animal
cat.eat()
cat.sleep()

print("\n--- Méthodes redéfinies ---")
dog.make_sound()  # Version Dog
cat.make_sound()  # Version Cat

print("\n--- Méthodes spécifiques ---")
dog.fetch()    # Spécifique à Dog
cat.scratch()  # Spécifique à Cat

print("\n--- Display ---")
dog.display()
cat.display()

# Vérification de l'héritage
print("\n--- Vérification type ---")
print(f"dog est un Dog? {isinstance(dog, Dog)}")
print(f"dog est un Animal? {isinstance(dog, Animal)}")
print(f"dog est un Cat? {isinstance(dog, Cat)}")

print(f"\nDog hérite d'Animal? {issubclass(Dog, Animal)}")
print(f"Cat hérite d'Animal? {issubclass(Cat, Animal)}")
print(f"Dog hérite de Cat? {issubclass(Dog, Cat)}")

"""
CONCEPTS CLÉS:

1. super():
   - Accède à la classe parent
   - super().__init__(...) appelle constructeur parent
   - super().methode() appelle méthode parent

2. Override (Redéfinition):
   - Même nom de méthode dans enfant
   - Remplace comportement parent
   - Peut appeler version parent avec super()

3. Extension:
   - Ajouter nouvelles méthodes/attributs
   - Enrichir fonctionnalité parent

4. isinstance() et issubclass():
   - isinstance(obj, Class): obj est instance de Class?
   - issubclass(Child, Parent): Child hérite de Parent?
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] SUPER() - ACCÈS À LA CLASSE PARENT
═══════════════════════════════════════════════════════════════
"""

"""
super() EN PYTHON:
- Retourne un proxy de la classe parent
- Permet d'appeler méthodes du parent
- Essentiel pour héritage multiple (MRO)

USAGES COURANTS:
1. super().__init__(...) - Appeler constructeur parent
2. super().methode() - Appeler méthode parent
3. Éviter nom en dur de la classe parent

PYTHON 2 vs PYTHON 3:
Python 2: super(ClasseEnfant, self).__init__(...)
Python 3: super().__init__(...)  # Plus simple!
"""

class Vehicle:
    """Véhicule générique."""
    
    def __init__(self, brand, max_speed):
        """Initialise un véhicule."""
        self.brand = brand
        self.max_speed = max_speed
        print(f"[Vehicle] {brand} créé")
    
    def start(self):
        """Démarre le véhicule."""
        print(f"{self.brand} démarre")
    
    def stop(self):
        """Arrête le véhicule."""
        print(f"{self.brand} s'arrête")
    
    def display_info(self):
        """Affiche les infos."""
        print(f"\nVéhicule: {self.brand}")
        print(f"Vitesse max: {self.max_speed} km/h")


class Car(Vehicle):
    """Voiture héritant de Vehicle."""
    
    def __init__(self, brand, max_speed, num_doors, fuel_type):
        """Initialise une voiture."""
        # 1. Appeler constructeur parent avec super()
        super().__init__(brand, max_speed)
        
        # 2. Initialiser attributs spécifiques
        self.num_doors = num_doors
        self.fuel_type = fuel_type
        print(f"[Car] Voiture {num_doors} portes créée")
    
    def start(self):
        """Démarre avec vérifications."""
        # Appeler méthode parent
        super().start()
        # Ajouter comportement spécifique
        print(f"  Vérification: {self.num_doors} portes")
        print(f"  Carburant: {self.fuel_type}")
    
    def display_info(self):
        """Affiche infos étendues."""
        # Réutiliser affichage parent
        super().display_info()
        # Ajouter infos spécifiques
        print(f"Portes: {self.num_doors}")
        print(f"Carburant: {self.fuel_type}")


class ElectricCar(Car):
    """Voiture électrique (héritage multiniveau)."""
    
    def __init__(self, brand, max_speed, num_doors, battery_capacity):
        """Initialise une voiture électrique."""
        # Appeler constructeur parent (Car)
        # Car appelle ensuite Vehicle
        super().__init__(brand, max_speed, num_doors, "Électrique")
        
        self.battery_capacity = battery_capacity
        self.battery_level = 100
        print(f"[ElectricCar] Batterie {battery_capacity} kWh")
    
    def start(self):
        """Démarre avec vérification batterie."""
        if self.battery_level > 0:
            super().start()
            print(f"  Batterie: {self.battery_level}%")
        else:
            print(f"[X] Batterie vide!")
    
    def charge(self, amount):
        """Recharge la batterie."""
        self.battery_level = min(100, self.battery_level + amount)
        print(f"[OK] Batterie rechargée à {self.battery_level}%")
    
    def display_info(self):
        """Affiche infos complètes."""
        super().display_info()
        print(f"Batterie: {self.battery_capacity} kWh")
        print(f"Niveau: {self.battery_level}%")


# Démonstration super()
print("\n=== SUPER() EN ACTION ===\n")

print("--- Création hiérarchique ---")
tesla = ElectricCar("Tesla Model 3", 225, 4, 75)

print("\n--- Méthodes avec super() ---")
tesla.start()
tesla.charge(20)

print("\n--- Display hiérarchique ---")
tesla.display_info()

"""
ORDRE D'EXÉCUTION AVEC super():

ElectricCar("Tesla", 225, 4, 75)
└─> super().__init__("Tesla", 225, 4, "Électrique")
    └─> Car.__init__()
        └─> super().__init__("Tesla", 225)
            └─> Vehicle.__init__()
                └─> Initialise brand, max_speed
            <- Retour à Car
            └─> Initialise num_doors, fuel_type
        <- Retour à ElectricCar
        └─> Initialise battery_capacity, battery_level

HIÉRARCHIE:
Vehicle (grand-parent)
    v
Car (parent)
    v
ElectricCar (enfant)
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] HÉRITAGE MULTIPLE
═══════════════════════════════════════════════════════════════
"""

"""
HÉRITAGE MULTIPLE:
- Une classe hérite de PLUSIEURS parents
- Python supporte nativement (contrairement à Java)
- Ordre des parents important (MRO)

SYNTAXE:
    class Enfant(Parent1, Parent2, Parent3):
        pass

UTILITÉ:
[OK] Combiner fonctionnalités de plusieurs classes
[OK] Mixins (classes de fonctionnalités)
[OK] Composition de comportements

ATTENTION:
- Problème du diamant
- MRO (Method Resolution Order)
- Complexité accrue
"""

# ═══ EXEMPLE: HÉRITAGE MULTIPLE ═══

class Flyable:
    """Capacité de voler."""
    
    def __init__(self):
        """Initialise capacité de vol."""
        self.altitude = 0
        print("[Flyable] Capacité de vol initialisée")
    
    def take_off(self):
        """Décolle."""
        self.altitude = 1000
        print(f"Décollage -> altitude {self.altitude}m")
    
    def land(self):
        """Atterrit."""
        self.altitude = 0
        print(f"Atterrissage -> altitude {self.altitude}m")
    
    def fly(self, altitude):
        """Vole à une altitude."""
        self.altitude = altitude
        print(f"Vol à {altitude}m")


class Swimmable:
    """Capacité de nager."""
    
    def __init__(self):
        """Initialise capacité de nage."""
        self.depth = 0
        print("[Swimmable] Capacité de nage initialisée")
    
    def swim(self):
        """Nage."""
        print("Nage à la surface")
    
    def dive(self, depth):
        """Plonge."""
        self.depth = depth
        print(f"Plongée à {depth}m de profondeur")


class Duck(Animal, Flyable, Swimmable):
    """
    Canard héritant de 3 classes!
    
    Hérite de:
    - Animal: name, age, eat(), sleep()
    - Flyable: altitude, take_off(), land(), fly()
    - Swimmable: depth, swim(), dive()
    """
    
    def __init__(self, name, age):
        """Initialise un canard."""
        # Initialiser tous les parents
        Animal.__init__(self, name, age)
        Flyable.__init__(self)
        Swimmable.__init__(self)
        print(f"[Duck] Canard {name} créé")
    
    def make_sound(self):
        """Le canard cancane."""
        print(f"{self.name} cancane: Coin! Coin!")


# Démonstration héritage multiple
print("\n=== HÉRITAGE MULTIPLE ===\n")

print("--- Création ---")
donald = Duck("Donald", 5)

print("\n--- Capacités multiples ---")
donald.eat()        # De Animal
donald.sleep()      # De Animal
donald.make_sound() # Redéfini dans Duck

donald.take_off()   # De Flyable
donald.fly(500)     # De Flyable
donald.land()       # De Flyable

donald.swim()       # De Swimmable
donald.dive(3)      # De Swimmable

# Vérifications
print("\n--- Vérifications ---")
print(f"isinstance(donald, Animal)? {isinstance(donald, Animal)}")
print(f"isinstance(donald, Flyable)? {isinstance(donald, Flyable)}")
print(f"isinstance(donald, Swimmable)? {isinstance(donald, Swimmable)}")

"""
═══════════════════════════════════════════════════════════════
  [OK] MRO (Method Resolution Order)
═══════════════════════════════════════════════════════════════
"""

"""
MRO - ORDRE DE RÉSOLUTION DES MÉTHODES:
- Détermine l'ordre de recherche des méthodes
- Crucial pour héritage multiple
- Algorithme C3 Linearization
- Accessible via: Class.__mro__ ou Class.mro()

RÈGLES:
1. Enfant avant parents
2. Parents dans l'ordre de déclaration
3. Cohérence (pas de cycles)

POURQUOI IMPORTANT?
- Évite ambiguïtés
- super() utilise MRO
- Résout le "problème du diamant"
"""

print("\n=== MRO (Method Resolution Order) ===\n")

# Afficher MRO de Duck
print("MRO de Duck:")
for i, cls in enumerate(Duck.__mro__, 1):
    print(f"{i}. {cls.__name__}")

"""
MRO de Duck:
1. Duck          # La classe elle-même
2. Animal        # Premier parent
3. Flyable       # Deuxième parent
4. Swimmable     # Troisième parent
5. object        # Base de tout

Quand on appelle duck.methode():
Python cherche dans cet ordre jusqu'à trouver!
"""

# ═══ PROBLÈME DU DIAMANT ═══

class A:
    """Classe base."""
    def method(self):
        print("Méthode de A")


class B(A):
    """Hérite de A."""
    def method(self):
        print("Méthode de B")
        super().method()


class C(A):
    """Hérite de A."""
    def method(self):
        print("Méthode de C")
        super().method()


class D(B, C):
    """Hérite de B et C (problème du diamant)."""
    def method(self):
        print("Méthode de D")
        super().method()


print("\n--- Problème du Diamant ---")
print("Hiérarchie:")
print("    A")
print("   / \\")
print("  B   C")
print("   \\ /")
print("    D")

d = D()
print("\nMRO de D:")
for cls in D.__mro__:
    print(f"  {cls.__name__}")

print("\nAppel d.method():")
d.method()

"""
RÉSULTAT:
Méthode de D
Méthode de B
Méthode de C
Méthode de A

MRO assure que A.method() n'est appelée QU'UNE FOIS!
C'est super() qui suit le MRO, pas l'héritage direct.
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] MIXINS - PATTERN POUR HÉRITAGE MULTIPLE
═══════════════════════════════════════════════════════════════
"""

"""
MIXIN:
- Classe conçue pour être héritée avec d'autres
- Fournit fonctionnalité spécifique
- Ne devrait pas être instanciée seule
- Convention: nom se termine par Mixin

CARACTÉRISTIQUES:
[OK] Petite et focalisée
[OK] Une seule responsabilité
[OK] Pas de __init__ (ou super().__init__())
[OK] Composition de comportements

USAGE:
class MaClasse(Mixin1, Mixin2, ClasseBase):
    pass
"""

class TimestampMixin:
    """
    Mixin ajoutant timestamps de création et modification.
    """
    
    def __init__(self, *args, **kwargs):
        """Initialise les timestamps."""
        super().__init__(*args, **kwargs)
        self.created_at = datetime.now()
        self.updated_at = datetime.now()
    
    def touch(self):
        """Met à jour le timestamp de modification."""
        self.updated_at = datetime.now()
    
    def get_age(self):
        """Retourne l'âge de l'objet."""
        return (datetime.now() - self.created_at).total_seconds()


class SerializableMixin:
    """
    Mixin ajoutant capacité de sérialisation.
    """
    
    def to_dict(self):
        """Convertit en dictionnaire."""
        # Exclure les attributs commençant par _
        return {
            key: value
            for key, value in self.__dict__.items()
            if not key.startswith('_')
        }
    
    def to_json(self):
        """Convertit en JSON."""
        import json
        data = self.to_dict()
        # Convertir datetime en string
        for key, value in data.items():
            if isinstance(value, datetime):
                data[key] = value.isoformat()
        return json.dumps(data, indent=2)


class ValidatableMixin:
    """
    Mixin ajoutant validation.
    """
    
    def validate(self):
        """
        Valide l'objet.
        Doit être redéfini dans la classe finale.
        """
        return True
    
    def is_valid(self):
        """Vérifie si l'objet est valide."""
        try:
            return self.validate()
        except Exception as e:
            print(f"[X] Validation échouée: {e}")
            return False


# Classe utilisant les mixins
class Article(TimestampMixin, SerializableMixin, ValidatableMixin):
    """
    Article de blog avec mixins.
    
    Hérite de:
    - TimestampMixin: created_at, updated_at, touch()
    - SerializableMixin: to_dict(), to_json()
    - ValidatableMixin: validate(), is_valid()
    """
    
    def __init__(self, title, content, author):
        """Initialise un article."""
        self.title = title
        self.content = content
        self.author = author
        self.published = False
        
        # Important: appeler super() pour les mixins
        super().__init__()
    
    def validate(self):
        """Valide l'article."""
        if not self.title or len(self.title) < 3:
            raise ValueError("Titre trop court")
        if not self.content or len(self.content) < 10:
            raise ValueError("Contenu trop court")
        if not self.author:
            raise ValueError("Auteur requis")
        return True
    
    def publish(self):
        """Publie l'article."""
        if self.is_valid():
            self.published = True
            self.touch()
            print(f"[OK] Article '{self.title}' publié")
        else:
            print(f"[X] Impossible de publier: article invalide")
    
    def edit(self, new_content):
        """Modifie le contenu."""
        self.content = new_content
        self.touch()
        print(f"[OK] Article modifié")


# Démonstration mixins
print("\n=== MIXINS ===\n")

article = Article(
    "Introduction à Python",
    "Python est un langage de programmation puissant et facile à apprendre...",
    "Alice"
)

print("--- Validation (ValidatableMixin) ---")
print(f"Article valide? {article.is_valid()}")

print("\n--- Publication ---")
article.publish()

print("\n--- Timestamps (TimestampMixin) ---")
print(f"Créé le: {article.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Modifié le: {article.updated_at.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Âge: {article.get_age():.2f} secondes")

import time
time.sleep(1)

print("\n--- Modification ---")
article.edit("Contenu mis à jour...")
print(f"Nouveau timestamp: {article.updated_at.strftime('%Y-%m-%d %H:%M:%S')}")

print("\n--- Sérialisation (SerializableMixin) ---")
print("Dict:")
print(article.to_dict())

print("\nJSON:")
print(article.to_json())

print("\n--- MRO de Article ---")
for cls in Article.__mro__:
    print(f"  {cls.__name__}")

"""
AVANTAGES MIXINS:
[OK] Réutilisation maximale
[OK] Composition flexible
[OK] Code DRY (Don't Repeat Yourself)
[OK] Séparation des responsabilités

CONVENTIONS:
- Nom se termine par Mixin
- Toujours appeler super().__init__()
- Petites et focalisées
- Documentation claire
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] EXEMPLE COMPLET: HIÉRARCHIE D'EMPLOYÉS
═══════════════════════════════════════════════════════════════
"""

class Employee:
    """Employé de base."""
    
    # Attribut de classe
    employee_count = 0
    base_raise_rate = 0.05
    
    def __init__(self, name, salary):
        """Initialise un employé."""
        self.name = name
        self.salary = salary
        self.hire_date = datetime.now()
        
        Employee.employee_count += 1
        self.employee_id = f"EMP{Employee.employee_count:04d}"
    
    def give_raise(self, rate=None):
        """Augmente le salaire."""
        if rate is None:
            rate = self.base_raise_rate
        
        old_salary = self.salary
        self.salary *= (1 + rate)
        print(f"[OK] {self.name}: {old_salary:,.0f} -> {self.salary:,.0f} FCFA")
    
    def display(self):
        """Affiche les infos."""
        print(f"\n{'='*60}")
        print(f"ID: {self.employee_id}")
        print(f"Nom: {self.name}")
        print(f"Salaire: {self.salary:,.0f} FCFA")
        print(f"Embauché: {self.hire_date.strftime('%Y-%m-%d')}")
        print(f"{'='*60}")


class Manager(Employee):
    """Manager avec bonus et équipe."""
    
    def __init__(self, name, salary, bonus):
        """Initialise un manager."""
        super().__init__(name, salary)
        self.bonus = bonus
        self.team = []
    
    def add_team_member(self, employee):
        """Ajoute un membre à l'équipe."""
        if employee not in self.team:
            self.team.append(employee)
            print(f"[OK] {employee.name} ajouté à l'équipe de {self.name}")
    
    def get_total_compensation(self):
        """Salaire total avec bonus."""
        return self.salary + self.bonus
    
    def display(self):
        """Affiche infos manager."""
        super().display()
        print(f"Bonus: {self.bonus:,.0f} FCFA")
        print(f"Compensation totale: {self.get_total_compensation():,.0f} FCFA")
        print(f"Taille équipe: {len(self.team)}")


class Developer(Employee):
    """Développeur avec langages et projets."""
    
    def __init__(self, name, salary, languages):
        """Initialise un développeur."""
        super().__init__(name, salary)
        self.languages = languages if isinstance(languages, list) else [languages]
        self.projects = []
    
    def add_language(self, language):
        """Ajoute un langage."""
        if language not in self.languages:
            self.languages.append(language)
            print(f"[OK] {self.name} apprend {language}")
    
    def assign_project(self, project):
        """Assigne un projet."""
        self.projects.append(project)
        print(f"[OK] Projet '{project}' assigné à {self.name}")
    
    def display(self):
        """Affiche infos développeur."""
        super().display()
        print(f"Langages: {', '.join(self.languages)}")
        print(f"Projets: {len(self.projects)}")


class SeniorDeveloper(Developer):
    """Développeur senior avec mentorat."""
    
    def __init__(self, name, salary, languages, mentees=None):
        """Initialise un développeur senior."""
        super().__init__(name, salary, languages)
        self.mentees = mentees or []
        # Augmentation plus importante
        self.base_raise_rate = 0.08
    
    def mentor(self, junior_dev):
        """Mentorise un junior."""
        if junior_dev not in self.mentees:
            self.mentees.append(junior_dev)
            print(f"[OK] {self.name} mentorise {junior_dev.name}")
    
    def display(self):
        """Affiche infos senior."""
        super().display()
        print(f"Mentorés: {len(self.mentees)}")
        if self.mentees:
            print(f"  - {', '.join(m.name for m in self.mentees)}")


# Démonstration hiérarchie complète
print("\n=== HIÉRARCHIE D'EMPLOYÉS ===\n")

# Créer employés
print("--- Création ---")
manager = Manager("Alice", 600_000, 100_000)
senior = SeniorDeveloper("Bob", 500_000, ["Python", "Java", "Go"])
dev1 = Developer("Charlie", 350_000, ["Python", "JavaScript"])
dev2 = Developer("Diana", 320_000, ["Java", "Kotlin"])

# Construire équipe
print("\n--- Construction équipe ---")
manager.add_team_member(senior)
manager.add_team_member(dev1)
manager.add_team_member(dev2)

# Mentorat
print("\n--- Mentorat ---")
senior.mentor(dev1)
senior.mentor(dev2)

# Projets
print("\n--- Projets ---")
senior.assign_project("Migration Cloud")
dev1.assign_project("Frontend Refactoring")
dev2.assign_project("API Gateway")

# Apprentissage
print("\n--- Apprentissage ---")
dev1.add_language("TypeScript")

# Augmentations
print("\n--- Augmentations annuelles ---")
manager.give_raise()
senior.give_raise()  # Utilise son taux plus élevé
dev1.give_raise()
dev2.give_raise(0.10)  # Augmentation exceptionnelle

# Affichage détaillé
manager.display()
senior.display()
dev1.display()

# Polymorphisme
print("\n--- Traitement polymorphe ---")
employees = [manager, senior, dev1, dev2]

print("\nTous les employés:")
for emp in employees:
    print(f"  {emp.employee_id}: {emp.name} - {emp.salary:,.0f} FCFA")

print(f"\nMasse salariale: {sum(e.salary for e in employees):,.0f} FCFA")

# Vérifications de type
print("\n--- Vérifications ---")
print(f"senior est Developer? {isinstance(senior, Developer)}")
print(f"senior est Employee? {isinstance(senior, Employee)}")
print(f"dev1 est SeniorDeveloper? {isinstance(dev1, SeniorDeveloper)}")

"""
═══════════════════════════════════════════════════════════════
  [GRAPHIQUE] RÉSUMÉ - PARTIE 3
═══════════════════════════════════════════════════════════════

HÉRITAGE SIMPLE:
[OK] class Enfant(Parent):
[OK] Hérite attributs et méthodes
[OK] Peut redéfinir (override)
[OK] Peut étendre

super():
[OK] Accède à la classe parent
[OK] super().__init__(...) pour constructeur
[OK] super().methode() pour méthodes
[OK] Suit le MRO en héritage multiple

HÉRITAGE MULTIPLE:
[OK] class C(A, B, D):
[OK] Combine plusieurs parents
[OK] MRO détermine l'ordre de résolution
[OK] Utile pour mixins

MRO (Method Resolution Order):
[OK] Ordre de recherche des méthodes
[OK] Algorithme C3 Linearization
[OK] Accessible via Class.__mro__
[OK] Résout le problème du diamant

MIXINS:
[OK] Classes de fonctionnalités
[OK] Nom finit par Mixin
[OK] Ne pas instancier seules
[OK] Toujours appeler super().__init__()

BONNES PRATIQUES:
1. Privilégier composition sur héritage (souvent)
2. Héritage pour relation IS-A claire
3. Mixins pour fonctionnalités réutilisables
4. Documenter hiérarchie
5. Vérifier MRO en héritage multiple

ERREURS COURANTES:
[X] Oublier super().__init__()
[X] Héritage trop profond (>3 niveaux)
[X] Héritage pour réutilisation (utiliser composition)
[X] Modifier attributs parent directement
[X] Ignorer le MRO

FIN DE LA PARTIE 3
═══════════════════════════════════════════════════════════════

PROCHAINE PARTIE: Polymorphisme et Méthodes Spéciales
- Duck typing
- Méthodes magiques (__str__, __repr__, __eq__, etc.)
- Surcharge d'opérateurs
- Protocols
"""

# ═══════════════════════════════════════════════════════════════
# FIN PARTIE 3
# ═══════════════════════════════════════════════════════════════

"""
═══════════════════════════════════════════════════════════════
  [OK] PARTIE 4: POLYMORPHISME ET MÉTHODES SPÉCIALES
═══════════════════════════════════════════════════════════════
"""

"""
QU'EST-CE QUE LE POLYMORPHISME?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Du grec "plusieurs formes" - capacité d'utiliser une interface
commune pour des types différents.

EN PYTHON:
- Duck Typing: "Si ça marche comme un canard..."
- Pas de types stricts
- Flexibilité maximale
- Méthodes spéciales (__magic__)

TYPES DE POLYMORPHISME:
1. Duck Typing (pythonique)
2. Héritage (classique)
3. Méthodes spéciales (protocoles)

PHILOSOPHIE:
"We don't care about what something IS,
 we care about what it CAN DO"
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] DUCK TYPING - "IF IT WALKS LIKE A DUCK..."
═══════════════════════════════════════════════════════════════
"""

"""
DUCK TYPING:
"If it walks like a duck and quacks like a duck,
 then it must be a duck"

PRINCIPE:
- Type de l'objet importe peu
- Ce qui compte: interface (méthodes disponibles)
- Pas de vérification de type à la compilation
- EAFP: "Easier to Ask Forgiveness than Permission"

AVANTAGES:
[OK] Flexibilité extrême
[OK] Code générique
[OK] Pas de hiérarchie rigide
[OK] Facilite les tests (mocking)
"""

class Duck:
    """Canard réel."""
    
    def swim(self):
        print("Le canard nage dans l'eau")
    
    def quack(self):
        print("Coin coin!")


class Person:
    """Personne imitant un canard."""
    
    def swim(self):
        print("La personne nage (pas très bien)")
    
    def quack(self):
        print("La personne imite: Coin coin!")


class RubberDuck:
    """Canard en plastique."""
    
    def swim(self):
        print("Le canard en plastique flotte")
    
    def quack(self):
        print("Couic couic! (son plastique)")


class Robot:
    """Robot sans capacités de canard."""
    
    def walk(self):
        print("Le robot marche")


def make_it_quack(duck):
    """
    Fait caqueter n'importe quoi qui peut caqueter.
    
    Duck typing: on ne vérifie PAS le type,
    on essaie juste d'appeler les méthodes!
    """
    # On ne vérifie PAS: if isinstance(duck, Duck)
    # On essaie juste!
    duck.swim()
    duck.quack()


# Démonstration duck typing
print("=== DUCK TYPING ===\n")

duck = Duck()
person = Person()
rubber_duck = RubberDuck()
robot = Robot()

print("--- Canard réel ---")
make_it_quack(duck)

print("\n--- Personne (a les bonnes méthodes!) ---")
make_it_quack(person)

print("\n--- Canard en plastique ---")
make_it_quack(rubber_duck)

print("\n--- Robot (n'a pas les méthodes) ---")
try:
    make_it_quack(robot)
except AttributeError as e:
    print(f"[X] Erreur: {e}")

"""
DUCK TYPING EN ACTION:

Tous ces objets sont acceptés par make_it_quack():
- Duck (le "vrai" canard)
- Person (imite un canard)
- RubberDuck (canard en plastique)

Mais Robot échoue car n'a pas swim() et quack()

IMPORTANT:
- Pas de type checking
- Pas d'héritage commun requis
- Juste besoin des bonnes méthodes
- Erreur à l'exécution si méthode manquante
"""

# ═══ EAFP vs LBYL ═══

"""
EAFP: Easier to Ask Forgiveness than Permission
- Style pythonique
- Essayer, gérer exception si erreur

LBYL: Look Before You Leap
- Style traditionnel
- Vérifier avant d'essayer
"""

# Style LBYL (moins pythonique)
def process_lbyl(obj):
    """Style: regarder avant de sauter."""
    if hasattr(obj, 'swim') and hasattr(obj, 'quack'):
        obj.swim()
        obj.quack()
    else:
        print("[X] Objet ne peut pas nager/caqueter")


# Style EAFP (pythonique!)
def process_eafp(obj):
    """Style: demander pardon plutôt que permission."""
    try:
        obj.swim()
        obj.quack()
    except AttributeError:
        print("[X] Objet ne peut pas nager/caqueter")


print("\n=== EAFP vs LBYL ===\n")

print("--- LBYL (Look Before You Leap) ---")
process_lbyl(duck)
process_lbyl(robot)

print("\n--- EAFP (Easier to Ask Forgiveness) ---")
process_eafp(duck)
process_eafp(robot)

"""
PRÉFÉREZ EAFP EN PYTHON!

Raisons:
1. Plus pythonique
2. Plus rapide (pas de double vérification)
3. Gère cas d'erreur rare
4. Code plus clair
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] MÉTHODES SPÉCIALES (MAGIC METHODS / DUNDER METHODS)
═══════════════════════════════════════════════════════════════
"""

"""
MÉTHODES SPÉCIALES:
- Commencent et finissent par __ (double underscore)
- Appelées automatiquement par Python
- Définissent comportement avec opérateurs, fonctions built-in
- Aussi appelées "magic methods" ou "dunder methods"

CATÉGORIES:
1. Représentation: __str__, __repr__
2. Comparaison: __eq__, __lt__, __gt__, etc.
3. Opérateurs: __add__, __mul__, __sub__, etc.
4. Conteneurs: __len__, __getitem__, __setitem__
5. Contexte: __enter__, __exit__
6. Attributs: __getattr__, __setattr__

POURQUOI?
[OK] Code pythonique et idiomatique
[OK] Intégration avec langage
[OK] Comportement naturel
[OK] Surcharge d'opérateurs
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] REPRÉSENTATION: __str__ et __repr__
═══════════════════════════════════════════════════════════════
"""

"""
__str__:
- Représentation "jolie" pour utilisateur
- Appelée par str(), print()
- Doit retourner une string lisible

__repr__:
- Représentation "technique" pour développeur
- Appelée par repr(), dans le REPL
- Devrait être non-ambiguë
- Idéalement: eval(repr(obj)) == obj

RÈGLE:
- Toujours définir __repr__
- Définir __str__ si représentation différente souhaitée
- Si pas de __str__, Python utilise __repr__
"""

class Point:
    """Point en 2D avec représentations."""
    
    def __init__(self, x, y):
        """Initialise un point."""
        self.x = x
        self.y = y
    
    def __repr__(self):
        """
        Représentation technique.
        
        Retourne une string qui, si évaluée,
        recrée l'objet.
        """
        return f"Point({self.x}, {self.y})"
    
    def __str__(self):
        """Représentation lisible pour utilisateur."""
        return f"({self.x}, {self.y})"


# Démonstration
print("\n=== __str__ et __repr__ ===\n")

p = Point(3, 4)

print("--- __str__ (print, str()) ---")
print(p)              # Appelle __str__
print(str(p))         # Appelle __str__

print("\n--- __repr__ (repr(), REPL) ---")
print(repr(p))        # Appelle __repr__

print("\n--- Dans une liste (utilise __repr__) ---")
points = [Point(1, 2), Point(3, 4), Point(5, 6)]
print(points)

print("\n--- Recréation depuis __repr__ ---")
p2_str = repr(p)
print(f"String: {p2_str}")
p2 = eval(p2_str)
print(f"Objet recréé: {p2}")

"""
═══════════════════════════════════════════════════════════════
  [OK] COMPARAISON: __eq__, __lt__, __le__, __gt__, __ge__, __ne__
═══════════════════════════════════════════════════════════════
"""

"""
MÉTHODES DE COMPARAISON:
__eq__(self, other)  : self == other
__ne__(self, other)  : self != other
__lt__(self, other)  : self < other
__le__(self, other)  : self <= other
__gt__(self, other)  : self > other
__ge__(self, other)  : self >= other

PAR DÉFAUT:
- == compare identité (id)
- Pour comparer valeurs, définir __eq__

@functools.total_ordering:
- Décorateur magique
- Définir __eq__ et UNE autre (__lt__ par ex)
- Génère automatiquement les autres
"""

from functools import total_ordering

@total_ordering
class Student:
    """Étudiant comparable par moyenne."""
    
    def __init__(self, name, average):
        """Initialise un étudiant."""
        self.name = name
        self.average = average
    
    def __repr__(self):
        """Représentation."""
        return f"Student('{self.name}', {self.average})"
    
    def __str__(self):
        """Représentation lisible."""
        return f"{self.name} (moy: {self.average})"
    
    def __eq__(self, other):
        """
        Égalité basée sur la moyenne.
        
        Args:
            other: Autre étudiant
            
        Returns:
            bool: True si moyennes égales
        """
        if not isinstance(other, Student):
            return NotImplemented
        return self.average == other.average
    
    def __lt__(self, other):
        """
        Inférieur basé sur la moyenne.
        
        Avec @total_ordering, suffit pour générer:
        __le__, __gt__, __ge__
        """
        if not isinstance(other, Student):
            return NotImplemented
        return self.average < other.average


# Démonstration comparaisons
print("\n=== COMPARAISONS ===\n")

alice = Student("Alice", 15.5)
bob = Student("Bob", 14.0)
charlie = Student("Charlie", 15.5)

print("--- Égalité (__eq__) ---")
print(f"alice == bob: {alice == bob}")
print(f"alice == charlie: {alice == charlie}")
print(f"alice != bob: {alice != bob}")

print("\n--- Comparaisons (__lt__, etc.) ---")
print(f"alice < bob: {alice < bob}")
print(f"alice > bob: {alice > bob}")
print(f"alice <= charlie: {alice <= charlie}")
print(f"alice >= bob: {alice >= bob}")

print("\n--- Tri (utilise __lt__) ---")
students = [bob, alice, charlie, Student("Diana", 16.0)]
students_sorted = sorted(students)
print("Étudiants triés par moyenne:")
for s in students_sorted:
    print(f"  {s}")

print("\n--- min/max (utilisent comparaisons) ---")
print(f"Meilleur: {max(students)}")
print(f"Plus faible: {min(students)}")

"""
IMPORTANT - NotImplemented:

Dans __eq__, __lt__, etc., retourner NotImplemented
(pas NotImplementedError!) si types incompatibles.

Permet à Python d'essayer la comparaison inverse:
a == b échoue -> Python essaie b == a
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] OPÉRATEURS ARITHMÉTIQUES
═══════════════════════════════════════════════════════════════
"""

"""
SURCHARGE D'OPÉRATEURS:
__add__(self, other)       : self + other
__sub__(self, other)       : self - other
__mul__(self, other)       : self * other
__truediv__(self, other)   : self / other
__floordiv__(self, other)  : self // other
__mod__(self, other)       : self % other
__pow__(self, other)       : self ** other

VERSIONS IN-PLACE:
__iadd__, __isub__, etc.   : +=, -=, etc.

VERSIONS INVERSÉES:
__radd__, __rsub__, etc.   : other + self
"""

class Vector2D:
    """Vecteur 2D avec opérateurs surchargés."""
    
    def __init__(self, x, y):
        """Initialise un vecteur."""
        self.x = x
        self.y = y
    
    def __repr__(self):
        """Représentation."""
        return f"Vector2D({self.x}, {self.y})"
    
    def __str__(self):
        """Représentation lisible."""
        return f"<{self.x}, {self.y}>"
    
    # ADDITION
    def __add__(self, other):
        """Addition de vecteurs: v1 + v2"""
        if isinstance(other, Vector2D):
            return Vector2D(self.x + other.x, self.y + other.y)
        return NotImplemented
    
    # SOUSTRACTION
    def __sub__(self, other):
        """Soustraction: v1 - v2"""
        if isinstance(other, Vector2D):
            return Vector2D(self.x - other.x, self.y - other.y)
        return NotImplemented
    
    # MULTIPLICATION (par scalaire)
    def __mul__(self, scalar):
        """Multiplication par scalaire: v * 3"""
        if isinstance(scalar, (int, float)):
            return Vector2D(self.x * scalar, self.y * scalar)
        return NotImplemented
    
    # MULTIPLICATION INVERSÉE
    def __rmul__(self, scalar):
        """Multiplication inversée: 3 * v"""
        return self.__mul__(scalar)
    
    # DIVISION
    def __truediv__(self, scalar):
        """Division par scalaire: v / 2"""
        if isinstance(scalar, (int, float)):
            if scalar == 0:
                raise ValueError("Division par zéro")
            return Vector2D(self.x / scalar, self.y / scalar)
        return NotImplemented
    
    # NÉGATION
    def __neg__(self):
        """Négation: -v"""
        return Vector2D(-self.x, -self.y)
    
    # VALEUR ABSOLUE
    def __abs__(self):
        """Module du vecteur: abs(v)"""
        return (self.x ** 2 + self.y ** 2) ** 0.5
    
    # ÉGALITÉ
    def __eq__(self, other):
        """Égalité de vecteurs."""
        if not isinstance(other, Vector2D):
            return NotImplemented
        return self.x == other.x and self.y == other.y
    
    # Méthode utilitaire
    def dot(self, other):
        """Produit scalaire."""
        return self.x * other.x + self.y * other.y


# Démonstration opérateurs
print("\n=== SURCHARGE D'OPÉRATEURS ===\n")

v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)

print(f"v1 = {v1}")
print(f"v2 = {v2}")

print("\n--- Addition (__add__) ---")
v3 = v1 + v2
print(f"v1 + v2 = {v3}")

print("\n--- Soustraction (__sub__) ---")
v4 = v1 - v2
print(f"v1 - v2 = {v4}")

print("\n--- Multiplication (__mul__) ---")
v5 = v1 * 2
print(f"v1 * 2 = {v5}")

print("\n--- Multiplication inversée (__rmul__) ---")
v6 = 3 * v1
print(f"3 * v1 = {v6}")

print("\n--- Division (__truediv__) ---")
v7 = v1 / 2
print(f"v1 / 2 = {v7}")

print("\n--- Négation (__neg__) ---")
v8 = -v1
print(f"-v1 = {v8}")

print("\n--- Valeur absolue (__abs__) ---")
print(f"|v1| = {abs(v1)}")

print("\n--- Égalité (__eq__) ---")
print(f"v1 == v2: {v1 == v2}")
print(f"v1 == Vector2D(3, 4): {v1 == Vector2D(3, 4)}")

print("\n--- Combinaisons ---")
result = (v1 + v2) * 2 - v1 / 2
print(f"(v1 + v2) * 2 - v1 / 2 = {result}")

"""
═══════════════════════════════════════════════════════════════
  [OK] CONTENEURS: __len__, __getitem__, __setitem__, __contains__
═══════════════════════════════════════════════════════════════
"""

"""
PROTOCOLE CONTENEUR:
__len__(self)               : len(obj)
__getitem__(self, key)      : obj[key]
__setitem__(self, key, val) : obj[key] = val
__delitem__(self, key)      : del obj[key]
__contains__(self, item)    : item in obj
__iter__(self)              : for item in obj

Implémenter ces méthodes fait de votre objet un "conteneur"
compatible avec les opérations Python standard.
"""

class PlayList:
    """Playlist de musique comme un conteneur."""
    
    def __init__(self, name):
        """Initialise une playlist."""
        self.name = name
        self._songs = []
    
    def __repr__(self):
        """Représentation."""
        return f"PlayList('{self.name}', {len(self._songs)} songs)"
    
    def __str__(self):
        """Représentation lisible."""
        return f"[SON] {self.name} ({len(self._songs)} chansons)"
    
    # LONGUEUR
    def __len__(self):
        """Nombre de chansons: len(playlist)"""
        return len(self._songs)
    
    # ACCÈS PAR INDEX
    def __getitem__(self, index):
        """
        Accès par index: playlist[0]
        
        Supporte aussi le slicing: playlist[1:3]
        """
        return self._songs[index]
    
    # MODIFICATION PAR INDEX
    def __setitem__(self, index, song):
        """Modification: playlist[0] = 'New Song'"""
        self._songs[index] = song
    
    # SUPPRESSION PAR INDEX
    def __delitem__(self, index):
        """Suppression: del playlist[0]"""
        del self._songs[index]
    
    # APPARTENANCE
    def __contains__(self, song):
        """Vérification: 'Song' in playlist"""
        return song in self._songs
    
    # ITÉRATION
    def __iter__(self):
        """Permet: for song in playlist"""
        return iter(self._songs)
    
    # ADDITION (concaténation)
    def __add__(self, other):
        """Fusion de playlists: pl1 + pl2"""
        if isinstance(other, PlayList):
            new_pl = PlayList(f"{self.name} + {other.name}")
            new_pl._songs = self._songs + other._songs
            return new_pl
        return NotImplemented
    
    # Méthodes utilitaires
    def add(self, song):
        """Ajoute une chanson."""
        self._songs.append(song)
        print(f"[OK] '{song}' ajoutée à {self.name}")
    
    def remove(self, song):
        """Retire une chanson."""
        if song in self._songs:
            self._songs.remove(song)
            print(f"[OK] '{song}' retirée de {self.name}")
    
    def shuffle(self):
        """Mélange la playlist."""
        import random
        random.shuffle(self._songs)
        print(f"[OK] {self.name} mélangée")


# Démonstration protocole conteneur
print("\n=== PROTOCOLE CONTENEUR ===\n")

# Créer playlist
pl = PlayList("Ma Playlist")

# Ajouter chansons
print("--- Ajout de chansons ---")
pl.add("Bohemian Rhapsody")
pl.add("Stairway to Heaven")
pl.add("Imagine")
pl.add("Hotel California")

# LONGUEUR
print(f"\n--- __len__ ---")
print(f"Nombre de chansons: {len(pl)}")

# ACCÈS PAR INDEX
print(f"\n--- __getitem__ ---")
print(f"Première chanson: {pl[0]}")
print(f"Dernière chanson: {pl[-1]}")
print(f"Chansons 1-2: {pl[1:3]}")

# MODIFICATION
print(f"\n--- __setitem__ ---")
print(f"Avant: {pl[0]}")
pl[0] = "We Will Rock You"
print(f"Après: {pl[0]}")

# APPARTENANCE
print(f"\n--- __contains__ ---")
print(f"'Imagine' dans playlist? {'Imagine' in pl}")
print(f"'Yesterday' dans playlist? {'Yesterday' in pl}")

# ITÉRATION
print(f"\n--- __iter__ ---")
print("Toutes les chansons:")
for i, song in enumerate(pl, 1):
    print(f"  {i}. {song}")

# SUPPRESSION
print(f"\n--- __delitem__ ---")
print(f"Avant suppression: {len(pl)} chansons")
del pl[1]
print(f"Après suppression: {len(pl)} chansons")

# ADDITION
print(f"\n--- __add__ ---")
pl2 = PlayList("Rock Classics")
pl2.add("Sweet Child O' Mine")
pl2.add("Back in Black")

pl3 = pl + pl2
print(f"Fusion: {pl3}")
print(f"  Total: {len(pl3)} chansons")

"""
AVANTAGES PROTOCOLE CONTENEUR:

[OK] Syntaxe naturelle Python
[OK] Compatible avec built-ins (len, in, etc.)
[OK] Fonctione avec compréhensions
[OK] Support du slicing
[OK] Itération facile
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] MÉTHODES SPÉCIALES AVANCÉES
═══════════════════════════════════════════════════════════════
"""

"""
AUTRES MÉTHODES IMPORTANTES:

__call__(self, ...)      : obj(...) - objet appelable
__bool__(self)           : bool(obj), if obj:
__hash__(self)           : hash(obj) - utilisation dans dict/set
__format__(self, spec)   : f"{obj:spec}"
__enter__, __exit__      : with obj: (context manager)
"""

class Counter:
    """Compteur appelable."""
    
    def __init__(self, start=0):
        """Initialise le compteur."""
        self.count = start
    
    def __call__(self, increment=1):
        """
        Rend l'objet appelable: counter()
        
        Args:
            increment: Valeur à ajouter
            
        Returns:
            int: Nouvelle valeur
        """
        self.count += increment
        return self.count
    
    def __repr__(self):
        """Représentation."""
        return f"Counter({self.count})"


class Temperature:
    """Température avec formatage personnalisé."""
    
    def __init__(self, celsius):
        """Initialise en Celsius."""
        self.celsius = celsius
    
    def __repr__(self):
        """Représentation."""
        return f"Temperature({self.celsius})"
    
    def __format__(self, spec):
        """
        Formatage personnalisé.
        
        Formats supportés:
        - 'c' ou '': Celsius
        - 'f': Fahrenheit
        - 'k': Kelvin
        """
        if spec == '' or spec == 'c':
            return f"{self.celsius}°C"
        elif spec == 'f':
            fahrenheit = self.celsius * 9/5 + 32
            return f"{fahrenheit}°F"
        elif spec == 'k':
            kelvin = self.celsius + 273.15
            return f"{kelvin}K"
        else:
            raise ValueError(f"Format inconnu: {spec}")
    
    def __bool__(self):
        """
        Vrai si température > 0°C.
        
        Permet: if temperature:
        """
        return self.celsius > 0


# Démonstration méthodes avancées
print("\n=== MÉTHODES SPÉCIALES AVANCÉES ===\n")

# __call__
print("--- __call__ (objet appelable) ---")
counter = Counter(10)
print(f"Initial: {counter}")
print(f"Après counter(): {counter()}")
print(f"Après counter(5): {counter(5)}")
print(f"Après counter(): {counter()}")

# __format__
print("\n--- __format__ (formatage) ---")
temp = Temperature(25)
print(f"Celsius: {temp:c}")
print(f"Fahrenheit: {temp:f}")
print(f"Kelvin: {temp:k}")
print(f"Défaut: {temp}")

# __bool__
print("\n--- __bool__ (conversion booléenne) ---")
temp_pos = Temperature(20)
temp_neg = Temperature(-5)
temp_zero = Temperature(0)

print(f"20°C est vrai? {bool(temp_pos)}")
print(f"-5°C est vrai? {bool(temp_neg)}")
print(f"0°C est vrai? {bool(temp_zero)}")

if temp_pos:
    print("[OK] Température positive!")
if not temp_neg:
    print("[OK] Température négative ou nulle!")

"""
═══════════════════════════════════════════════════════════════
  [OK] EXEMPLE COMPLET: CLASSE MONEY
═══════════════════════════════════════════════════════════════
"""

@total_ordering
class Money:
    """
    Montant d'argent avec opérations et formatage.
    
    Démontre de nombreuses méthodes spéciales.
    """
    
    # Taux de change (simplifié)
    EXCHANGE_RATES = {
        'FCFA': 1.0,
        'EUR': 655.957,
        'USD': 600.0,
    }
    
    def __init__(self, amount, currency='FCFA'):
        """
        Initialise un montant.
        
        Args:
            amount: Montant numérique
            currency: Devise ('FCFA', 'EUR', 'USD')
        """
        if currency not in self.EXCHANGE_RATES:
            raise ValueError(f"Devise inconnue: {currency}")
        
        self.amount = float(amount)
        self.currency = currency
    
    # REPRÉSENTATION
    def __repr__(self):
        """Représentation technique."""
        return f"Money({self.amount}, '{self.currency}')"
    
    def __str__(self):
        """Représentation lisible."""
        return f"{self.amount:,.2f} {self.currency}"
    
    def __format__(self, spec):
        """
        Formatage personnalisé.
        
        Formats:
        - '': Format par défaut
        - '.0f': Sans décimales
        - '.2f': 2 décimales (défaut)
        """
        if spec == '':
            return str(self)
        else:
            formatted = f"{self.amount:{spec}}"
            return f"{formatted} {self.currency}"
    
    # CONVERSION
    def to_currency(self, target_currency):
        """Convertit vers une autre devise."""
        if target_currency not in self.EXCHANGE_RATES:
            raise ValueError(f"Devise inconnue: {target_currency}")
        
        # Convertir vers FCFA puis vers devise cible
        fcfa_amount = self.amount * self.EXCHANGE_RATES[self.currency]
        target_amount = fcfa_amount / self.EXCHANGE_RATES[target_currency]
        
        return Money(target_amount, target_currency)
    
    # OPÉRATEURS ARITHMÉTIQUES
    def __add__(self, other):
        """Addition: m1 + m2"""
        if isinstance(other, Money):
            # Convertir other vers notre devise
            other_converted = other.to_currency(self.currency)
            return Money(self.amount + other_converted.amount, self.currency)
        elif isinstance(other, (int, float)):
            return Money(self.amount + other, self.currency)
        return NotImplemented
    
    def __radd__(self, other):
        """Addition inversée."""
        return self.__add__(other)
    
    def __sub__(self, other):
        """Soustraction: m1 - m2"""
        if isinstance(other, Money):
            other_converted = other.to_currency(self.currency)
            return Money(self.amount - other_converted.amount, self.currency)
        elif isinstance(other, (int, float)):
            return Money(self.amount - other, self.currency)
        return NotImplemented
    
    def __mul__(self, scalar):
        """Multiplication: m * 2"""
        if isinstance(scalar, (int, float)):
            return Money(self.amount * scalar, self.currency)
        return NotImplemented
    
    def __rmul__(self, scalar):
        """Multiplication inversée: 2 * m"""
        return self.__mul__(scalar)
    
    def __truediv__(self, scalar):
        """Division: m / 2"""
        if isinstance(scalar, (int, float)):
            if scalar == 0:
                raise ValueError("Division par zéro")
            return Money(self.amount / scalar, self.currency)
        return NotImplemented
    
    def __neg__(self):
        """Négation: -m"""
        return Money(-self.amount, self.currency)
    
    def __abs__(self):
        """Valeur absolue: abs(m)"""
        return Money(abs(self.amount), self.currency)
    
    # COMPARAISONS
    def __eq__(self, other):
        """Égalité: m1 == m2"""
        if isinstance(other, Money):
            other_converted = other.to_currency(self.currency)
            return abs(self.amount - other_converted.amount) < 0.01
        elif isinstance(other, (int, float)):
            return abs(self.amount - other) < 0.01
        return NotImplemented
    
    def __lt__(self, other):
        """Inférieur: m1 < m2"""
        if isinstance(other, Money):
            other_converted = other.to_currency(self.currency)
            return self.amount < other_converted.amount
        elif isinstance(other, (int, float)):
            return self.amount < other
        return NotImplemented
    
    # UTILITAIRES
    def __bool__(self):
        """Vrai si montant > 0."""
        return self.amount > 0
    
    def __float__(self):
        """Conversion en float: float(m)"""
        return self.amount
    
    def __int__(self):
        """Conversion en int: int(m)"""
        return int(self.amount)
    
    def __hash__(self):
        """Hash pour utilisation dans dict/set."""
        # Convertir en FCFA pour hash cohérent
        fcfa_amount = self.amount * self.EXCHANGE_RATES[self.currency]
        return hash((round(fcfa_amount, 2), 'FCFA'))


# Démonstration Money
print("\n=== CLASSE MONEY COMPLÈTE ===\n")

# Création
m1 = Money(100_000, 'FCFA')
m2 = Money(50, 'EUR')
m3 = Money(100, 'USD')

print("--- Création et Représentation ---")
print(f"m1 = {m1}")
print(f"m2 = {m2}")
print(f"m3 = {m3}")
print(f"repr(m1) = {repr(m1)}")

print("\n--- Formatage ---")
print(f"Sans décimales: {m1:.0f}")
print(f"Avec décimales: {m1:.2f}")

print("\n--- Conversion de devises ---")
m2_fcfa = m2.to_currency('FCFA')
m3_fcfa = m3.to_currency('FCFA')
print(f"50 EUR = {m2_fcfa}")
print(f"100 USD = {m3_fcfa}")

print("\n--- Opérations arithmétiques ---")
total = m1 + m2  # Devises différentes, auto-conversion!
print(f"{m1} + {m2} = {total}")

diff = m1 - Money(20_000, 'FCFA')
print(f"{m1} - 20,000 FCFA = {diff}")

double = m1 * 2
print(f"{m1} * 2 = {double}")

half = m1 / 2
print(f"{m1} / 2 = {half}")

print("\n--- Comparaisons ---")
print(f"{m1} == {Money(100_000, 'FCFA')}: {m1 == Money(100_000, 'FCFA')}")
print(f"{m1} > {m2}: {m1 > m2}")
print(f"{m1} < {m3_fcfa}: {m1 < m3_fcfa}")

print("\n--- Tri ---")
amounts = [m1, m2, m3, Money(75_000, 'FCFA')]
sorted_amounts = sorted(amounts)
print("Montants triés:")
for amount in sorted_amounts:
    print(f"  {amount} = {amount.to_currency('FCFA')}")

print("\n--- Conversions ---")
print(f"float(m1) = {float(m1)}")
print(f"int(m1) = {int(m1)}")
print(f"bool(m1) = {bool(m1)}")
print(f"bool(Money(0)) = {bool(Money(0))}")

print("\n--- Utilisation dans set/dict (hash) ---")
money_set = {m1, Money(100_000, 'FCFA'), m2}
print(f"Set (dédupliqué): {len(money_set)} éléments")

"""
═══════════════════════════════════════════════════════════════
  [GRAPHIQUE] RÉSUMÉ - PARTIE 4
═══════════════════════════════════════════════════════════════

DUCK TYPING:
[OK] "Si ça marche comme un canard..."
[OK] Type importe peu, interface compte
[OK] EAFP > LBYL (pythonique)
[OK] Flexibilité maximale

MÉTHODES SPÉCIALES:
[OK] __str__, __repr__ : Représentation
[OK] __eq__, __lt__, etc. : Comparaison
[OK] __add__, __mul__, etc. : Opérateurs
[OK] __len__, __getitem__, etc. : Conteneurs
[OK] __call__ : Objet appelable
[OK] __format__ : Formatage personnalisé
[OK] __bool__ : Conversion booléenne
[OK] __hash__ : Utilisation dans dict/set

@total_ordering:
[OK] Définir __eq__ + une comparaison
[OK] Génère automatiquement les autres

BONNES PRATIQUES:
1. Toujours définir __repr__
2. Utiliser @total_ordering pour comparaisons
3. Retourner NotImplemented si type incompatible
4. __hash__ cohérent avec __eq__
5. Documenter format specs pour __format__

ERREURS COURANTES:
[X] Oublier __repr__
[X] __hash__ sans __eq__
[X] Retourner None au lieu de NotImplemented
[X] __str__ trop verbeux
[X] Modifier self dans __add__ (retourner nouveau!)

FIN DE LA PARTIE 4
═══════════════════════════════════════════════════════════════

PROCHAINE PARTIE: Classes Abstraites et Interfaces
- ABC (Abstract Base Classes)
- @abstractmethod
- Protocols (Python 3.8+)
- Interfaces pythoniques
"""

# ═══════════════════════════════════════════════════════════════
# FIN PARTIE 4
# ═══════════════════════════════════════════════════════════════

"""
═══════════════════════════════════════════════════════════════
  [OK] PARTIE 5: CLASSES ABSTRAITES ET PROTOCOLS
═══════════════════════════════════════════════════════════════
"""

"""
QU'EST-CE QU'UNE CLASSE ABSTRAITE?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Classe qui ne peut PAS être instanciée directement.
Sert de template pour les sous-classes.

EN PYTHON:
- Module abc (Abstract Base Classes)
- @abstractmethod pour méthodes abstraites
- Depuis Python 3.8: Protocols (duck typing formalisé)

POURQUOI?
[OK] Définir contrat/interface
[OK] Forcer implémentation dans sous-classes
[OK] Documentation du design
[OK] Éviter code incomplet
[OK] Polymorphisme garanti
"""

from abc import ABC, abstractmethod

"""
═══════════════════════════════════════════════════════════════
  [OK] ABC - ABSTRACT BASE CLASS
═══════════════════════════════════════════════════════════════
"""

"""
UTILISATION:
1. Hériter de ABC
2. Décorer méthodes avec @abstractmethod
3. Sous-classes DOIVENT implémenter méthodes abstraites
4. Impossible d'instancier si méthodes non implémentées

SYNTAXE:
    from abc import ABC, abstractmethod
    
    class MaClasse(ABC):
        @abstractmethod
        def ma_methode(self):
            pass
"""

class Shape(ABC):
    """
    Forme géométrique abstraite.
    
    Toute forme DOIT implémenter:
    - calculate_area()
    - calculate_perimeter()
    """
    
    def __init__(self, color):
        """Initialise une forme."""
        self.color = color
    
    @abstractmethod
    def calculate_area(self):
        """
        Calcule l'aire.
        
        DOIT être implémentée dans sous-classes.
        
        Returns:
            float: Aire de la forme
        """
        pass
    
    @abstractmethod
    def calculate_perimeter(self):
        """
        Calcule le périmètre.
        
        Returns:
            float: Périmètre de la forme
        """
        pass
    
    # Méthode concrète (non-abstraite)
    def display_info(self):
        """Affiche les informations."""
        print(f"\n{self.__class__.__name__}")
        print(f"Couleur: {self.color}")
        print(f"Aire: {self.calculate_area():.2f}")
        print(f"Périmètre: {self.calculate_perimeter():.2f}")


class Circle(Shape):
    """Cercle (implémente Shape)."""
    
    def __init__(self, color, radius):
        """Initialise un cercle."""
        super().__init__(color)
        self.radius = radius
    
    # Implémentation OBLIGATOIRE
    def calculate_area(self):
        """Calcule l'aire du cercle."""
        import math
        return math.pi * self.radius ** 2
    
    def calculate_perimeter(self):
        """Calcule le périmètre (circonférence)."""
        import math
        return 2 * math.pi * self.radius


class Rectangle(Shape):
    """Rectangle (implémente Shape)."""
    
    def __init__(self, color, length, width):
        """Initialise un rectangle."""
        super().__init__(color)
        self.length = length
        self.width = width
    
    def calculate_area(self):
        """Calcule l'aire du rectangle."""
        return self.length * self.width
    
    def calculate_perimeter(self):
        """Calcule le périmètre."""
        return 2 * (self.length + self.width)


# Classe INCOMPLÈTE (ne compile pas!)
# class IncompleteShape(Shape):
#     def __init__(self, color):
#         super().__init__(color)
#     # [X] Manque calculate_area et calculate_perimeter
#     # TypeError à l'instanciation!


# Démonstration ABC
print("=== CLASSES ABSTRAITES (ABC) ===\n")

# Impossible d'instancier Shape
print("--- Tentative d'instancier Shape ---")
try:
    shape = Shape("rouge")
except TypeError as e:
    print(f"[X] Erreur: {e}")

# Classes concrètes OK
print("\n--- Classes concrètes ---")
circle = Circle("bleu", 5)
rectangle = Rectangle("vert", 10, 6)

circle.display_info()
rectangle.display_info()

# Polymorphisme garanti
print("\n--- Polymorphisme garanti ---")
shapes = [circle, rectangle, Circle("rouge", 3)]

for shape in shapes:
    # Garanti d'avoir calculate_area grâce à ABC!
    print(f"{shape.__class__.__name__}: aire = {shape.calculate_area():.2f}")

"""
AVANTAGES ABC:
[OK] Contrat explicite
[OK] Erreur à l'instanciation si incomplet
[OK] Documentation du design
[OK] IDE peut aider (autocomplétion)
[OK] Polymorphisme sûr
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] @abstractmethod AVANCÉ
═══════════════════════════════════════════════════════════════
"""

"""
@abstractmethod peut être combiné avec:
- @property
- @classmethod
- @staticmethod

Permet de définir propriétés et méthodes abstraites.
"""

class Vehicle(ABC):
    """Véhicule abstrait avec propriétés."""
    
    def __init__(self, brand):
        """Initialise un véhicule."""
        self._brand = brand
    
    # Propriété abstraite
    @property
    @abstractmethod
    def max_speed(self):
        """Vitesse maximale (propriété abstraite)."""
        pass
    
    # Méthode abstraite normale
    @abstractmethod
    def start(self):
        """Démarre le véhicule."""
        pass
    
    # Méthode de classe abstraite
    @classmethod
    @abstractmethod
    def get_type(cls):
        """Retourne le type de véhicule."""
        pass
    
    # Méthode concrète
    def display(self):
        """Affiche les infos."""
        print(f"{self._brand} - {self.get_type()}")
        print(f"Vitesse max: {self.max_speed} km/h")


class Car(Vehicle):
    """Voiture (implémente Vehicle)."""
    
    def __init__(self, brand, max_speed_value):
        """Initialise une voiture."""
        super().__init__(brand)
        self._max_speed = max_speed_value
    
    @property
    def max_speed(self):
        """Implémente propriété abstraite."""
        return self._max_speed
    
    def start(self):
        """Implémente méthode abstraite."""
        print(f"{self._brand} démarre: Vroom!")
    
    @classmethod
    def get_type(cls):
        """Implémente méthode de classe abstraite."""
        return "Voiture"


# Démonstration
print("\n=== @abstractmethod AVANCÉ ===\n")

car = Car("Toyota", 180)
car.display()
car.start()

"""
═══════════════════════════════════════════════════════════════
  [OK] PROTOCOLS (Python 3.8+) - Duck Typing Formalisé
═══════════════════════════════════════════════════════════════
"""

"""
PROTOCOLS:
- Introduits en Python 3.8 (PEP 544)
- Duck typing avec type checking
- Pas besoin d'héritage explicite
- Vérification structurelle (pas nominale)

DIFFÉRENCE ABC vs PROTOCOL:
ABC: Héritage obligatoire
Protocol: Juste avoir les bonnes méthodes

UTILISATION:
    from typing import Protocol
    
    class MonProtocol(Protocol):
        def methode(self) -> ReturnType:
            ...
"""

from typing import Protocol, runtime_checkable

@runtime_checkable
class Drawable(Protocol):
    """
    Protocol pour objets dessinables.
    
    Tout objet avec draw() est Drawable,
    PAS besoin d'hériter explicitement.
    """
    
    def draw(self) -> None:
        """Dessine l'objet."""
        ...


class Square:
    """Carré (implémente Drawable SANS hériter)."""
    
    def __init__(self, side):
        """Initialise un carré."""
        self.side = side
    
    def draw(self):
        """Dessine le carré."""
        print(f"Dessin d'un carré {self.side}x{self.side}")


class Tree:
    """Arbre (implémente aussi Drawable)."""
    
    def __init__(self, height):
        """Initialise un arbre."""
        self.height = height
    
    def draw(self):
        """Dessine l'arbre."""
        print(f"Dessin d'un arbre de {self.height}m")


class Person:
    """Personne (N'implémente PAS Drawable)."""
    
    def walk(self):
        """Marche."""
        print("La personne marche")


def render(obj: Drawable) -> None:
    """
    Rend un objet Drawable.
    
    Type hint vérifié par mypy/IDE.
    """
    obj.draw()


# Démonstration Protocols
print("\n=== PROTOCOLS ===\n")

square = Square(10)
tree = Tree(5)
person = Person()

print("--- Vérification runtime ---")
print(f"Square est Drawable? {isinstance(square, Drawable)}")
print(f"Tree est Drawable? {isinstance(tree, Drawable)}")
print(f"Person est Drawable? {isinstance(person, Drawable)}")

print("\n--- Rendu ---")
render(square)
render(tree)

# person n'est pas Drawable
try:
    render(person)  # Fonctionne à runtime mais IDE/mypy signale!
except AttributeError as e:
    print(f"[X] Erreur: {e}")

"""
PROTOCOLS vs ABC:

ABC (Héritage nominal):
    class MyClass(AbstractClass):  # Héritage explicite
        pass

PROTOCOL (Héritage structurel):
    class MyClass:  # Pas d'héritage!
        def method(self): pass  # Suffit d'avoir la méthode

QUAND UTILISER?
- ABC: Hiérarchie claire, contrat fort
- Protocol: Duck typing avec type hints
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] EXEMPLE COMPLET: SYSTÈME DE PAIEMENT
═══════════════════════════════════════════════════════════════
"""

from abc import ABC, abstractmethod
from datetime import datetime
from typing import Protocol

# ═══ CLASSE ABSTRAITE ═══

class PaymentMethod(ABC):
    """
    Méthode de paiement abstraite.
    
    Toute méthode de paiement DOIT implémenter:
    - validate()
    - process_payment()
    - get_reference()
    """
    
    def __init__(self, holder_name):
        """Initialise une méthode de paiement."""
        self.holder_name = holder_name
        self._transactions = []
    
    @abstractmethod
    def validate(self) -> bool:
        """Valide la méthode de paiement."""
        pass
    
    @abstractmethod
    def process_payment(self, amount: float) -> bool:
        """
        Traite un paiement.
        
        Args:
            amount: Montant à payer
            
        Returns:
            bool: True si succès
        """
        pass
    
    @abstractmethod
    def get_reference(self) -> str:
        """Retourne une référence de transaction."""
        pass
    
    # Méthode concrète
    def _log_transaction(self, amount, status):
        """Enregistre une transaction."""
        self._transactions.append({
            'timestamp': datetime.now(),
            'amount': amount,
            'status': status,
            'reference': self.get_reference()
        })
    
    def get_history(self):
        """Retourne l'historique."""
        return self._transactions.copy()


class CreditCard(PaymentMethod):
    """Carte de crédit."""
    
    def __init__(self, holder_name, card_number, cvv, expiry):
        """Initialise une carte de crédit."""
        super().__init__(holder_name)
        self.card_number = card_number
        self.cvv = cvv
        self.expiry = expiry
        self.limit = 1_000_000
    
    def validate(self) -> bool:
        """Valide la carte."""
        if len(self.card_number) != 16:
            print("[X] Numéro de carte invalide")
            return False
        if len(self.cvv) != 3:
            print("[X] CVV invalide")
            return False
        print("[OK] Carte validée")
        return True
    
    def process_payment(self, amount: float) -> bool:
        """Traite un paiement par carte."""
        if not self.validate():
            self._log_transaction(amount, "failed")
            return False
        
        if amount > self.limit:
            print(f"[X] Dépassement limite: {self.limit:,.0f}")
            self._log_transaction(amount, "failed")
            return False
        
        print(f"[OK] Paiement CB de {amount:,.0f} FCFA effectué")
        self._log_transaction(amount, "success")
        return True
    
    def get_reference(self) -> str:
        """Génère référence."""
        return f"CC-{self.card_number[-4:]}-{datetime.now().timestamp()}"


class MobileMoney(PaymentMethod):
    """Paiement mobile money."""
    
    def __init__(self, holder_name, phone_number, provider):
        """Initialise mobile money."""
        super().__init__(holder_name)
        self.phone_number = phone_number
        self.provider = provider
    
    def validate(self) -> bool:
        """Valide le compte."""
        if len(self.phone_number) < 9:
            print("[X] Numéro invalide")
            return False
        print(f"[OK] Compte {self.provider} validé")
        return True
    
    def process_payment(self, amount: float) -> bool:
        """Traite un paiement mobile money."""
        if not self.validate():
            self._log_transaction(amount, "failed")
            return False
        
        print(f"[OK] Paiement {self.provider} de {amount:,.0f} FCFA")
        print(f"  Numéro: {self.phone_number}")
        self._log_transaction(amount, "success")
        return True
    
    def get_reference(self) -> str:
        """Génère référence."""
        return f"{self.provider}-{self.phone_number}-{datetime.now().timestamp()}"


# ═══ PROTOCOL ═══

@runtime_checkable
class Refundable(Protocol):
    """Protocol pour paiements remboursables."""
    
    def refund(self, amount: float) -> bool:
        """Rembourse un montant."""
        ...


# Implémenter Protocol dans CreditCard
class RefundableCreditCard(CreditCard):
    """Carte de crédit avec remboursement."""
    
    def refund(self, amount: float) -> bool:
        """Rembourse un montant."""
        print(f"[OK] Remboursement de {amount:,.0f} FCFA sur carte")
        self._log_transaction(-amount, "refund")
        return True


# ═══ PROCESSEUR DE PAIEMENTS ═══

class PaymentProcessor:
    """Traite les paiements de façon polymorphe."""
    
    def process(self, payment_method: PaymentMethod, amount: float) -> bool:
        """
        Traite un paiement.
        
        Accepte N'IMPORTE QUELLE PaymentMethod.
        """
        print(f"\n--- Traitement paiement ---")
        print(f"Méthode: {payment_method.__class__.__name__}")
        print(f"Titulaire: {payment_method.holder_name}")
        print(f"Montant: {amount:,.0f} FCFA")
        
        return payment_method.process_payment(amount)
    
    def refund_if_possible(self, payment_method, amount: float) -> bool:
        """Rembourse si possible (utilise Protocol)."""
        if isinstance(payment_method, Refundable):
            return payment_method.refund(amount)
        else:
            print(f"[X] {payment_method.__class__.__name__} ne supporte pas les remboursements")
            return False


# Démonstration système de paiement
print("\n=== SYSTÈME DE PAIEMENT COMPLET ===\n")

# Impossible d'instancier PaymentMethod
try:
    pm = PaymentMethod("Test")
except TypeError as e:
    print(f"[X] Impossible d'instancier classe abstraite")

# Créer méthodes de paiement
print("\n--- Création des méthodes ---")
cc = RefundableCreditCard("Alice", "1234567890123456", "123", "12/25")
mm = MobileMoney("Bob", "771234567", "Orange Money")

# Processeur
processor = PaymentProcessor()

# Traiter paiements (polymorphisme)
print("\n=== TRAITEMENT POLYMORPHE ===")
processor.process(cc, 50_000)
processor.process(mm, 25_000)

# Remboursements (Protocol)
print("\n=== REMBOURSEMENTS (Protocol) ===")
processor.refund_if_possible(cc, 10_000)   # [OK] Refundable
processor.refund_if_possible(mm, 5_000)    # [X] Pas Refundable

# Historique
print("\n=== HISTORIQUE ===")
print(f"\nHistorique carte ({len(cc.get_history())} transactions):")
for trans in cc.get_history():
    print(f"  {trans['timestamp'].strftime('%H:%M:%S')} | "
          f"{trans['amount']:>10,.0f} | {trans['status']}")

"""
CE QUE CET EXEMPLE DÉMONTRE:

1. ABC (PaymentMethod):
   [OK] Contrat clair pour toutes les méthodes de paiement
   [OK] Impossible d'oublier une méthode
   [OK] Code partagé (historique)

2. POLYMORPHISME:
   [OK] PaymentProcessor accepte toute PaymentMethod
   [OK] Traitement uniforme

3. PROTOCOL (Refundable):
   [OK] Fonctionnalité optionnelle
   [OK] Pas besoin de tout refactoriser
   [OK] Duck typing avec vérification
"""

"""
═══════════════════════════════════════════════════════════════
  [GRAPHIQUE] RÉSUMÉ - PARTIE 5
═══════════════════════════════════════════════════════════════

ABC (Abstract Base Class):
[OK] from abc import ABC, abstractmethod
[OK] Hériter de ABC
[OK] @abstractmethod pour méthodes abstraites
[OK] Impossible d'instancier si incomplet
[OK] Contrat explicite et forcé

@abstractmethod:
[OK] Peut combiner avec @property
[OK] Peut combiner avec @classmethod/@staticmethod
[OK] Sous-classes DOIVENT implémenter

PROTOCOLS (Python 3.8+):
[OK] from typing import Protocol
[OK] @runtime_checkable pour isinstance()
[OK] Duck typing formalisé
[OK] Pas besoin d'héritage
[OK] Vérification structurelle

ABC vs PROTOCOL:
┌──────────┬─────────────┬──────────────┐
│          │ ABC         │ Protocol     │
├──────────┼─────────────┼──────────────┤
│ Héritage │ Obligatoire │ Optionnel    │
│ Type     │ Nominal     │ Structurel   │
│ Quand    │ Hiérarchie  │ Duck typing  │
│          │ claire      │ + types      │
└──────────┴─────────────┴──────────────┘

BONNES PRATIQUES:
1. ABC pour hiérarchies claires
2. Protocol pour duck typing formalisé
3. @abstractmethod pour contrat
4. @runtime_checkable si isinstance() nécessaire
5. Documenter les contracts

FIN DE LA PARTIE 5
═══════════════════════════════════════════════════════════════

PROCHAINE PARTIE: Composition et Design Patterns
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] PARTIE 6: COMPOSITION ET DESIGN PATTERNS
═══════════════════════════════════════════════════════════════
"""

"""
COMPOSITION vs HÉRITAGE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

"Favor composition over inheritance"
- Design Patterns (Gang of Four)

HÉRITAGE:
- Relation "EST-UN" (is-a)
- Hiérarchie rigide
- Couplage fort
- Réutilisation verticale

COMPOSITION:
- Relation "A-UN" (has-a)
- Structure flexible
- Couplage faible
- Réutilisation horizontale

QUAND UTILISER QUOI?
[OK] Héritage: Relation IS-A claire et stable
[OK] Composition: Tout le reste!
"""

# ═══ PROBLÈME AVEC HÉRITAGE ═══

print("=== COMPOSITION vs HÉRITAGE ===\n")

# Approche par héritage (problématique)
class Animal:
    def eat(self):
        print("Mange")


class FlyingAnimal(Animal):
    def fly(self):
        print("Vole")


class SwimmingAnimal(Animal):
    def swim(self):
        print("Nage")


# Problème: Comment créer un canard qui vole ET nage?
# class Duck(FlyingAnimal, SwimmingAnimal):  # Héritage multiple compliqué
#     pass

# ═══ SOLUTION PAR COMPOSITION ═══

class FlyBehavior:
    """Comportement de vol (composant)."""
    
    def fly(self):
        """Vole."""
        print("Je vole dans les airs!")


class SwimBehavior:
    """Comportement de nage (composant)."""
    
    def swim(self):
        """Nage."""
        print("Je nage dans l'eau!")


class WalkBehavior:
    """Comportement de marche."""
    
    def walk(self):
        """Marche."""
        print("Je marche sur terre!")


class AnimalComposition:
    """Animal avec composition de comportements."""
    
    def __init__(self, name, *behaviors):
        """
        Initialise un animal avec comportements.
        
        Args:
            name: Nom de l'animal
            *behaviors: Comportements à composer
        """
        self.name = name
        self.behaviors = behaviors
    
    def perform_behaviors(self):
        """Exécute tous les comportements."""
        print(f"\n{self.name} peut:")
        for behavior in self.behaviors:
            # Trouver et appeler méthodes publiques
            for method_name in dir(behavior):
                if not method_name.startswith('_'):
                    method = getattr(behavior, method_name)
                    if callable(method):
                        method()


# Démonstration composition
print("--- Composition de comportements ---\n")

# Canard: vole, nage ET marche
duck = AnimalComposition(
    "Canard",
    FlyBehavior(),
    SwimBehavior(),
    WalkBehavior()
)
duck.perform_behaviors()

# Poisson: nage seulement
fish = AnimalComposition(
    "Poisson",
    SwimBehavior()
)
fish.perform_behaviors()

# Oiseau: vole et marche
bird = AnimalComposition(
    "Oiseau",
    FlyBehavior(),
    WalkBehavior()
)
bird.perform_behaviors()

"""
AVANTAGES COMPOSITION:
[OK] Flexibilité maximale
[OK] Réutilisation facile
[OK] Couplage faible
[OK] Changement de comportement à runtime
[OK] Pas de problème de héritage multiple
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] DESIGN PATTERNS COURANTS
═══════════════════════════════════════════════════════════════
"""

"""
DESIGN PATTERNS:
- Solutions éprouvées à problèmes récurrents
- Gang of Four (GoF): 23 patterns classiques
- Catégories: Création, Structure, Comportement

EN PYTHON:
Certains patterns sont simplifiés grâce à:
- First-class functions
- Duck typing
- Décorateurs
- Context managers
"""

# ═══ PATTERN: SINGLETON ═══

"""
SINGLETON:
- Une seule instance de la classe
- Point d'accès global

USAGE:
Configuration, Logger, Connection Pool
"""

class Singleton:
    """Implémentation Singleton simple."""
    
    _instance = None
    
    def __new__(cls):
        """Crée ou retourne l'instance unique."""
        if cls._instance is None:
            print("Création de l'instance Singleton")
            cls._instance = super().__new__(cls)
        return cls._instance
    
    def __init__(self):
        """Initialise (appelé à chaque fois!)."""
        if not hasattr(self, 'initialized'):
            print("Initialisation du Singleton")
            self.initialized = True
            self.data = []


# Version avec décorateur (pythonique)
def singleton(cls):
    """Décorateur Singleton."""
    instances = {}
    
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    
    return get_instance


@singleton
class Logger:
    """Logger Singleton avec décorateur."""
    
    def __init__(self):
        """Initialise le logger."""
        print("Logger créé")
        self.logs = []
    
    def log(self, message):
        """Enregistre un message."""
        self.logs.append(f"[{datetime.now()}] {message}")
        print(f"LOG: {message}")


# Démonstration Singleton
print("\n=== PATTERN: SINGLETON ===\n")

print("--- Singleton simple ---")
s1 = Singleton()
s2 = Singleton()
print(f"s1 is s2: {s1 is s2}")  # True

print("\n--- Singleton avec décorateur ---")
logger1 = Logger()
logger2 = Logger()
print(f"logger1 is logger2: {logger1 is logger2}")  # True

logger1.log("Message 1")
logger2.log("Message 2")
print(f"Logs (via logger1): {len(logger1.logs)}")
print(f"Logs (via logger2): {len(logger2.logs)}")

# ═══ PATTERN: FACTORY ═══

"""
FACTORY:
- Crée des objets sans spécifier classe exacte
- Délègue instanciation à des sous-classes

USAGE:
Création d'objets complexes, plugin systems
"""

class Document(ABC):
    """Document abstrait."""
    
    @abstractmethod
    def create_content(self):
        """Crée le contenu."""
        pass
    
    def save(self, filename):
        """Sauvegarde le document."""
        content = self.create_content()
        print(f"Sauvegarde de {self.__class__.__name__} dans {filename}")
        print(f"Contenu: {content[:50]}...")


class PDFDocument(Document):
    """Document PDF."""
    
    def __init__(self, title):
        self.title = title
    
    def create_content(self):
        """Crée contenu PDF."""
        return f"%PDF-1.4\nTitle: {self.title}\n..."


class WordDocument(Document):
    """Document Word."""
    
    def __init__(self, title):
        self.title = title
    
    def create_content(self):
        """Crée contenu Word."""
        return f"<docx>\n<title>{self.title}</title>\n...</docx>"


class DocumentFactory:
    """Factory pour créer documents."""
    
    @staticmethod
    def create_document(doc_type, title):
        """
        Crée un document selon le type.
        
        Args:
            doc_type: Type de document ('pdf', 'word')
            title: Titre du document
            
        Returns:
            Document: Instance du document
        """
        if doc_type == 'pdf':
            return PDFDocument(title)
        elif doc_type == 'word':
            return WordDocument(title)
        else:
            raise ValueError(f"Type inconnu: {doc_type}")


# Démonstration Factory
print("\n=== PATTERN: FACTORY ===\n")

# Créer documents via factory
pdf = DocumentFactory.create_document('pdf', "Mon Rapport")
word = DocumentFactory.create_document('word', "Ma Lettre")

pdf.save("rapport.pdf")
word.save("lettre.docx")

# ═══ PATTERN: STRATEGY ═══

"""
STRATEGY:
- Définit famille d'algorithmes
- Encapsule chacun
- Les rend interchangeables

USAGE:
Différentes façons de faire la même chose
"""

class SortStrategy(ABC):
    """Stratégie de tri abstraite."""
    
    @abstractmethod
    def sort(self, data):
        """Trie les données."""
        pass


class BubbleSortStrategy(SortStrategy):
    """Tri à bulles."""
    
    def sort(self, data):
        """Tri à bulles (simplifié)."""
        print("Tri à bulles...")
        return sorted(data)  # Simplifié


class QuickSortStrategy(SortStrategy):
    """Tri rapide."""
    
    def sort(self, data):
        """Tri rapide (simplifié)."""
        print("Tri rapide...")
        return sorted(data)  # Simplifié


class Sorter:
    """Trieur avec stratégie configurable."""
    
    def __init__(self, strategy: SortStrategy):
        """Initialise avec une stratégie."""
        self._strategy = strategy
    
    def set_strategy(self, strategy: SortStrategy):
        """Change la stratégie."""
        self._strategy = strategy
    
    def sort(self, data):
        """Trie avec la stratégie actuelle."""
        return self._strategy.sort(data)


# Démonstration Strategy
print("\n=== PATTERN: STRATEGY ===\n")

data = [5, 2, 8, 1, 9]

# Créer trieur avec stratégie
sorter = Sorter(BubbleSortStrategy())
result1 = sorter.sort(data)
print(f"Résultat: {result1}")

# Changer stratégie à runtime
print()
sorter.set_strategy(QuickSortStrategy())
result2 = sorter.sort(data)
print(f"Résultat: {result2}")

# ═══ PATTERN: OBSERVER ═══

"""
OBSERVER:
- Dépendance un-à-plusieurs
- Quand un objet change, tous ses dépendants sont notifiés

USAGE:
Event systems, MVC, notifications
"""

class Subject:
    """Sujet observé."""
    
    def __init__(self):
        """Initialise le sujet."""
        self._observers = []
        self._state = None
    
    def attach(self, observer):
        """Attache un observateur."""
        if observer not in self._observers:
            self._observers.append(observer)
            print(f"[OK] {observer.__class__.__name__} attaché")
    
    def detach(self, observer):
        """Détache un observateur."""
        self._observers.remove(observer)
        print(f"[OK] {observer.__class__.__name__} détaché")
    
    def notify(self):
        """Notifie tous les observateurs."""
        for observer in self._observers:
            observer.update(self)
    
    @property
    def state(self):
        """État actuel."""
        return self._state
    
    @state.setter
    def state(self, value):
        """Change l'état et notifie."""
        print(f"\n[ANNONCE] État changé: {self._state} -> {value}")
        self._state = value
        self.notify()


class Observer(ABC):
    """Observateur abstrait."""
    
    @abstractmethod
    def update(self, subject):
        """Appelé quand le sujet change."""
        pass


class EmailObserver(Observer):
    """Observateur email."""
    
    def update(self, subject):
        """Envoie email."""
        print(f"  [EMAIL] Email: État = {subject.state}")


class SMSObserver(Observer):
    """Observateur SMS."""
    
    def update(self, subject):
        """Envoie SMS."""
        print(f"  [MOBILE] SMS: État = {subject.state}")


class LogObserver(Observer):
    """Observateur log."""
    
    def update(self, subject):
        """Log."""
        print(f"  [NOTE] Log: État changé à {subject.state}")


# Démonstration Observer
print("\n=== PATTERN: OBSERVER ===\n")

# Créer sujet
subject = Subject()

# Créer observateurs
email_obs = EmailObserver()
sms_obs = SMSObserver()
log_obs = LogObserver()

# Attacher observateurs
subject.attach(email_obs)
subject.attach(sms_obs)
subject.attach(log_obs)

# Changer état -> notifie tous
subject.state = "En cours"
subject.state = "Terminé"

# Détacher un observateur
print()
subject.detach(sms_obs)
subject.state = "Archivé"

"""
═══════════════════════════════════════════════════════════════
  [GRAPHIQUE] RÉSUMÉ - PARTIE 6
═══════════════════════════════════════════════════════════════

COMPOSITION vs HÉRITAGE:
[OK] Préférer composition pour flexibilité
[OK] Héritage si relation IS-A claire
[OK] Composition: couplage faible

DESIGN PATTERNS:
[OK] Singleton: Instance unique
[OK] Factory: Création déléguée
[OK] Strategy: Algorithmes interchangeables
[OK] Observer: Notifications automatiques

PRINCIPES:
[OK] DRY (Don't Repeat Yourself)
[OK] SOLID (Single Responsibility, etc.)
[OK] Separation of Concerns
[OK] Low Coupling, High Cohesion

FIN DE LA PARTIE 6
═══════════════════════════════════════════════════════════════
"""

"""
═══════════════════════════════════════════════════════════════
  [OK] PARTIE 7: CONCEPTS AVANCÉS ET DATACLASSES
═══════════════════════════════════════════════════════════════
"""

"""
DATACLASSES (Python 3.7+)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Simplifie la création de classes pour stocker données.

GÉNÈRE AUTOMATIQUEMENT:
- __init__
- __repr__
- __eq__
- __hash__ (optionnel)
- __lt__, etc. (optionnel)

AVANTAGES:
[OK] Moins de boilerplate
[OK] Code plus lisible
[OK] Type hints intégrés
[OK] Valeurs par défaut
[OK] Immuabilité (frozen=True)
"""

from dataclasses import dataclass, field
from typing import List

# ═══ DATACLASS BASIQUE ═══

@dataclass
class Point:
    """Point 2D avec dataclass."""
    x: float
    y: float
    
    def distance_from_origin(self) -> float:
        """Calcule distance depuis origine."""
        return (self.x ** 2 + self.y ** 2) ** 0.5


# Équivalent sans dataclass:
# class Point:
#     def __init__(self, x, y):
#         self.x = x
#         self.y = y
#     
#     def __repr__(self):
#         return f"Point(x={self.x}, y={self.y})"
#     
#     def __eq__(self, other):
#         ...


# Démonstration dataclass basique
print("=== DATACLASSES ===\n")

p1 = Point(3, 4)
p2 = Point(3, 4)
p3 = Point(5, 6)

print(f"p1 = {p1}")  # __repr__ auto-généré
print(f"p1 == p2: {p1 == p2}")  # __eq__ auto-généré
print(f"p1 == p3: {p1 == p3}")
print(f"Distance: {p1.distance_from_origin()}")

# ═══ DATACLASS AVEC OPTIONS ═══

@dataclass(order=True, frozen=False)
class Student:
    """
    Étudiant avec dataclass.
    
    order=True: Génère __lt__, __le__, __gt__, __ge__
    frozen=False: Modifiable (True = immuable)
    """
    name: str
    age: int
    grade: float
    courses: List[str] = field(default_factory=list)
    
    def __post_init__(self):
        """Appelé après __init__."""
        print(f"[OK] Étudiant {self.name} créé")


# Démonstration options
print("\n--- Dataclass avec options ---\n")

alice = Student("Alice", 20, 15.5, ["Math", "Physique"])
bob = Student("Bob", 21, 14.0)  # courses utilise default_factory

print(f"alice = {alice}")
print(f"bob = {bob}")

# order=True permet comparaisons
print(f"\nalice > bob: {alice > bob}")  # Compare par ordre des champs

# ═══ DATACLASS IMMUABLE ═══

@dataclass(frozen=True)
class Config:
    """Configuration immuable."""
    host: str
    port: int
    debug: bool = False


config = Config("localhost", 8000)
print(f"\n--- Dataclass frozen ---")
print(config)

try:
    config.port = 9000  # [X] Erreur!
except Exception as e:
    print(f"[X] Impossible de modifier (frozen): {type(e).__name__}")

# ═══ FIELD OPTIONS ═══

from dataclasses import dataclass, field

@dataclass
class Product:
    """Produit avec field options."""
    name: str
    price: float
    
    # Valeur par défaut
    quantity: int = 0
    
    # Default factory (pour mutables)
    tags: List[str] = field(default_factory=list)
    
    # Exclu de __repr__
    _internal_id: str = field(default="", repr=False)
    
    # Exclu de __init__ (calculé)
    total_value: float = field(init=False)
    
    def __post_init__(self):
        """Calcule valeurs dérivées."""
        self.total_value = self.price * self.quantity


# Démonstration field
print("\n--- Field options ---\n")

prod = Product("Laptop", 500_000, 10)
print(prod)  # _internal_id pas affiché
print(f"Valeur totale: {prod.total_value:,.0f}")

"""
═══════════════════════════════════════════════════════════════
  [OK] DESCRIPTORS (AVANCÉ)
═══════════════════════════════════════════════════════════════
"""

"""
DESCRIPTORS:
- Objets qui définissent __get__, __set__, __delete__
- Contrôle accès aux attributs
- @property est implémenté avec descriptors

USAGE:
Validation réutilisable, lazy loading, etc.
"""

class Validator:
    """Descriptor pour validation."""
    
    def __init__(self, min_value=None, max_value=None):
        """Initialise le validateur."""
        self.min_value = min_value
        self.max_value = max_value
    
    def __set_name__(self, owner, name):
        """Appelé quand descriptor assigné à attribut."""
        self.name = name
        self.private_name = f'_{name}'
    
    def __get__(self, instance, owner):
        """Appelé lors de l'accès à l'attribut."""
        if instance is None:
            return self
        return getattr(instance, self.private_name)
    
    def __set__(self, instance, value):
        """Appelé lors de l'assignation."""
        if self.min_value is not None and value < self.min_value:
            raise ValueError(f"{self.name} doit être >= {self.min_value}")
        if self.max_value is not None and value > self.max_value:
            raise ValueError(f"{self.name} doit être <= {self.max_value}")
        setattr(instance, self.private_name, value)


class Person:
    """Personne avec validation par descriptors."""
    
    age = Validator(0, 150)
    height = Validator(0, 300)
    
    def __init__(self, name, age, height):
        """Initialise une personne."""
        self.name = name
        self.age = age  # Passe par Validator.__set__
        self.height = height


# Démonstration descriptors
print("\n=== DESCRIPTORS ===\n")

person = Person("Alice", 25, 165)
print(f"Âge: {person.age}")
print(f"Taille: {person.height}")

try:
    person.age = -5
except ValueError as e:
    print(f"[X] {e}")

try:
    person.height = 500
except ValueError as e:
    print(f"[X] {e}")

"""
═══════════════════════════════════════════════════════════════
  [OK] CONTEXT MANAGERS (__enter__, __exit__)
═══════════════════════════════════════════════════════════════
"""

"""
CONTEXT MANAGER:
- Gère setup/cleanup automatiquement
- Utilisé avec 'with'
- Garantit cleanup même si erreur

MÉTHODES:
- __enter__: Setup, retourne ressource
- __exit__: Cleanup, gère exceptions

USAGE:
Fichiers, connexions, locks, transactions
"""

class DatabaseConnection:
    """Connexion DB avec context manager."""
    
    def __init__(self, db_name):
        """Initialise la connexion."""
        self.db_name = db_name
        self.connected = False
    
    def __enter__(self):
        """
        Appelé en entrant dans 'with'.
        
        Returns:
            self: Retourné et assigné à 'as'
        """
        print(f"[PLUGIN] Connexion à {self.db_name}...")
        self.connected = True
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """
        Appelé en sortant de 'with'.
        
        Args:
            exc_type: Type d'exception (None si aucune)
            exc_val: Valeur d'exception
            exc_tb: Traceback
            
        Returns:
            bool: True pour supprimer exception
        """
        print(f"[PLUGIN] Déconnexion de {self.db_name}")
        self.connected = False
        
        if exc_type is not None:
            print(f"[ATTENTION] Exception capturée: {exc_type.__name__}")
        
        return False  # Ne supprime pas l'exception
    
    def query(self, sql):
        """Exécute une requête."""
        if not self.connected:
            raise RuntimeError("Pas connecté!")
        print(f"[GRAPHIQUE] Requête: {sql}")
        return ["Résultat 1", "Résultat 2"]


# Démonstration context manager
print("\n=== CONTEXT MANAGERS ===\n")

print("--- Utilisation normale ---")
with DatabaseConnection("ma_db") as db:
    results = db.query("SELECT * FROM users")
    print(f"Résultats: {results}")
# __exit__ appelé automatiquement

print("\n--- Avec exception ---")
try:
    with DatabaseConnection("ma_db") as db:
        results = db.query("SELECT * FROM users")
        raise ValueError("Oups!")  # Erreur
except ValueError:
    print("Exception gérée")
# __exit__ appelé MALGRÉ l'exception!

"""
ALTERNATIVE: @contextmanager
"""

from contextlib import contextmanager

@contextmanager
def open_file(filename, mode):
    """Context manager simple avec décorateur."""
    print(f"[DOSSIER] Ouverture {filename}")
    file = open(filename, mode)
    try:
        yield file  # Retourne le fichier
    finally:
        print(f"[DOSSIER] Fermeture {filename}")
        file.close()


"""
═══════════════════════════════════════════════════════════════
  [DOCS] RÉCAPITULATIF FINAL - POO EN PYTHON
═══════════════════════════════════════════════════════════════

PARTIE 1 - FONDAMENTAUX:
[OK] Classes et objets
[OK] __init__, self
[OK] Attributs instance/classe
[OK] @classmethod, @staticmethod

PARTIE 2 - ENCAPSULATION:
[OK] Conventions _, __
[OK] @property
[OK] Getters/setters pythoniques
[OK] Validation

PARTIE 3 - HÉRITAGE:
[OK] Héritage simple/multiple
[OK] super()
[OK] MRO
[OK] Mixins

PARTIE 4 - POLYMORPHISME:
[OK] Duck typing
[OK] Méthodes spéciales (__str__, __add__, etc.)
[OK] Surcharge d'opérateurs
[OK] Protocoles conteneur

PARTIE 5 - ABSTRACTION:
[OK] ABC, @abstractmethod
[OK] Protocols
[OK] Interfaces pythoniques

PARTIE 6 - DESIGN:
[OK] Composition vs Héritage
[OK] Design Patterns
[OK] SOLID principles

PARTIE 7 - AVANCÉ:
[OK] Dataclasses
[OK] Descriptors
[OK] Context managers

PRINCIPES CLÉ:
1. "We're all consenting adults" (pas de private strict)
2. "Explicit is better than implicit"
3. "Simple is better than complex"
4. EAFP > LBYL
5. Duck typing
6. Composition > Inheritance

BONNES PRATIQUES GLOBALES:
[OK] Type hints pour documentation
[OK] Docstrings pour toutes les classes/méthodes
[OK] Tests unitaires
[OK] PEP 8 (style guide)
[OK] SOLID principles
[OK] Design patterns appropriés
[OK] Keep it simple!

RESSOURCES:
- PEP 8: Style Guide
- PEP 257: Docstring Conventions
- PEP 484: Type Hints
- PEP 544: Protocols
- PEP 557: Dataclasses
- "Design Patterns" - Gang of Four
- "Effective Python" - Brett Slatkin

═══════════════════════════════════════════════════════════════
  [BRAVO] FÉLICITATIONS!
═══════════════════════════════════════════════════════════════

Vous maîtrisez maintenant la POO en Python de A à Z!

Ce guide couvre:
- Tous les concepts fondamentaux
- Techniques avancées
- Best practices
- Patterns courants
- Pièges à éviter

PROCHAINES ÉTAPES:
1. Pratiquer avec des projets réels
2. Étudier le code de bibliothèques populaires
3. Approfondir les design patterns
4. Explorer métaclasses (très avancé)
5. Contribuer à l'open source

Bon coding! [PYTHON]*
═══════════════════════════════════════════════════════════════
"""

# ═══════════════════════════════════════════════════════════════
# FIN DU GUIDE COMPLET POO PYTHON
# ═══════════════════════════════════════════════════════════════