Niveau : Licence 3 Module : Programmation Orientée Objet Volume : 1h30 (CM/TD) Langage : Python 3.10+

Chapitre 5 : La Programmation Orientée Objet en Python — Comparaison avec le C++

Découvrir comment les grands concepts de la POO s'expriment dans un langage interprété à typage dynamique fort. Comprendre la philosophie pythonique de l'encapsulation (« We are all consenting adults here »), exploiter le Duck Typing sans mot-clé virtual, décortiquer la résolution de l'héritage en diamant par le MRO (Method Resolution Order) et identifier avec précision ce qui est possible ou impossible en Python par rapport au C++.

Objectifs pédagogiques de la séance

  • Définir des classes en Python avec le paramètre explicite self et le constructeur __init__.
  • Comprendre l'encapsulation pythonique : conventions (_attribut), Name Mangling (__attribut) et le décorateur @property.
  • Mettre en œuvre l'héritage simple et la délégation au parent avec la fonction native super().
  • Assimiler le polymorphisme par Duck Typing (« Si ça marche comme un canard et fait coin-coin, c'est un canard ») et les interfaces formelles avec abc.ABC.
  • Résoudre le problème du diamant en Python : comprendre pourquoi aucun mot-clé virtual n'est requis grâce à l'algorithme de linéarisation C3 MRO.
  • Distinguer ce qui est possible et impossible en Python : surcharge de méthodes par signature, destruction déterministe (RAII vs with) et surcharge d'opérateurs magiques (dunder methods).
1

Classes, Instances et Encapsulation en Python

⏱️ 20 minutes

1.1. Anatomie d'une classe : Le rôle fondamental de self

En Python, une classe se définit avec le mot-clé class. Contrairement au C++ où le pointeur this est implicitement passé aux méthodes membres, Python exige que l'instance courante soit explicitement déclarée comme premier paramètre de chaque méthode, conventionnellement nommé self.

1.2. L'Encapsulation en Python : Philosophie des "Consenting Adults"

En C++, l'encapsulation est vérifiée et appliquée de manière stricte par le compilateur : tenter d'accéder à un membre private provoque une erreur de compilation immédiate.

En Python, il n'existe aucun mot-clé private ou protected. La communauté Python adhère à la maxime : « We are all consenting adults here » (nous sommes tous des adultes consentants et responsables). La protection repose sur des conventions reconnues :

🟡 1. Convention protégée : Un seul tiret _attribut

Préfixer un attribut d'un seul tiret bas (ex: self._solde) est un signal fort destiné aux développeurs : « Cet attribut est privé/interne à la classe, ne le modifiez pas directement depuis l'extérieur ». L'interpréteur Python ne bloque toutefois pas l'accès technique.

🔴 2. Attribut pseudo-privé : Double tiret __attribut (Name Mangling)

Préfixer un attribut de deux tirets bas (ex: self.__numero) active le Name Mangling (brouillage de nom). Python renomme automatiquement l'attribut en interne sous la forme _NomDeClasse__numero pour éviter les collisions accidentelles lors de l'héritage. Ce n'est pas un système de sécurité inviolable, mais une protection anti-écrasement.

1.3. Les Propriétés Pythoniques : Le décorateur @property

En C++, pour protéger un attribut, on écrit des méthodes getSolde() et setSolde(val). En Python, on utilise le décorateur @property qui permet d'accéder à la méthode avec la syntaxe naturelle d'un attribut (compte.solde = 500) tout en exécutant du code de validation en coulisses :

PYTHONpython_classes_encapsulation.py
  1. """
  2. Chapitre 5 - Exemple 1 : Classes, Constructeur et Encapsulation en Python
  3. Notion de self, convention de nommage, name mangling et décorateur @property
  4. """
  5.  
  6. class CompteBancaire:
  7. """Modélisation d'un compte bancaire avec validation des invariants."""
  8.  
  9. def __init__(self, titulaire: str, solde_initial: float = 0.0):
  10. # Attribut public conventionnel
  11. self.titulaire = titulaire
  12.  
  13. # Attribut "protégé" par convention (un seul tiret du bas : _solde)
  14. # Indique aux autres développeurs : "usage interne, ne pas modifier directement"
  15. self._solde = 0.0
  16.  
  17. # Attribut "privé" avec Name Mangling (deux tirets du bas : __numero_compte)
  18. # Python renomme automatiquement l'attribut en _CompteBancaire__numero_compte
  19. self.__numero_compte = f"FR76-{abs(hash(titulaire)) % 100000:05d}"
  20.  
  21. # Utilisation du setter pour garantir l'invariant (solde positif ou nul)
  22. self.solde = solde_initial
  23.  
  24. # Propriété (Getter) : accès naturel 'compte.solde'
  25. @property
  26. def solde(self) -> float:
  27. return self._solde
  28.  
  29. # Mutateur (Setter) avec contrôle de validité
  30. @solde.setter
  31. def solde(self, nouveau_solde: float):
  32. if nouveau_solde < 0:
  33. raise ValueError(f"Erreur : Le solde ne peut pas être négatif ({nouveau_solde} € demandé).")
  34. self._solde = nouveau_solde
  35.  
  36. @property
  37. def numero_compte(self) -> str:
  38. return self.__numero_compte
  39.  
  40. def deposer(self, montant: float):
  41. if montant <= 0:
  42. raise ValueError("Le montant déposé doit être strictement positif.")
  43. self._solde += montant
  44. print(f"[Dépôt] +{montant:.2f} € sur le compte de {self.titulaire}")
  45.  
  46. # Méthodes spéciales (Dunder methods) pour représentation textuelle
  47. def __str__(self) -> str:
  48. return f"Compte de {self.titulaire} ({self.__numero_compte}) : {self._solde:.2f} €"
  49.  
  50. def __repr__(self) -> str:
  51. return f"CompteBancaire(titulaire='{self.titulaire}', solde={self._solde})"
  52.  
  53.  
  54. if __name__ == "__main__":
  55. print("=== 1. Classes & Encapsulation en Python ===\n")
  56.  
  57. compte = CompteBancaire("Claire Dupont", 1500.0)
  58. print("Affichage via __str__ :", compte)
  59. print("Numéro de compte (lecture seule) :", compte.numero_compte)
  60.  
  61. # Dépôt
  62. compte.deposer(250.0)
  63. print("Nouveau solde via @property :", compte.solde, "€")
  64.  
  65. # Modification via setter avec validation
  66. compte.solde = 2000.0
  67. print("Solde après mise à jour :", compte.solde, "€")
  68.  
  69. # Tentative d'affectation invalide
  70. try:
  71. compte.solde = -500.0
  72. except ValueError as e:
  73. print(">> Validation d'invariant réussie :", e)
  74.  
  75. # DÉMONSTRATION DU "CONSENTING ADULTS" EN PYTHON :
  76. # En Python, le mot-clé 'private' de C++ n'existe pas !
  77. # L'attribut à double tiret __numero_compte est simplement "masqué" par Name Mangling :
  78. print("\n--- Analyse du 'Name Mangling' en Python ---")
  79. print("Accès direct à compte.__numero_compte...")
  80. try:
  81. print(compte.__numero_compte)
  82. except AttributeError as e:
  83. print(">> AttributeError déclenché :", e)
  84.  
  85. # Mais l'attribut est toujours accessible via son nom altéré par Python :
  86. nom_altere = compte._CompteBancaire__numero_compte
  87. print(">> Contournement via '_CompteBancaire__numero_compte' :", nom_altere)
  88. print("En Python : 'We are all consenting adults here' (l'encapsulation est une convention).")
2

L'Héritage Simple et la Fonction super()

⏱️ 15 minutes

2.1. Syntaxe de l'héritage en Python

La dérivation en Python s'écrit en plaçant la classe mère entre parenthèses après le nom de la classe fille :
class Manager(Employe):

Comme en C++, l'héritage modélise une relation stricte de type « EST-UN » (un Manager est un Employe). La classe fille hérite de tous les attributs et méthodes de la classe mère.

2.2. Délégation au parent avec super()

Pour initialiser les attributs hérités de la base, on appelle la fonction native super() :
super().__init__(nom, salaire_base)

Contrairement au C++ où l'appel parent s'écrit obligatoirement dans la liste d'initialisation (: Employe(nom, s)), en Python super().__init__() s'appelle directement à l'intérieur du corps de la méthode __init__.

💡 Introspection native des types

Python fournit deux fonctions intégrées très performantes pour interroger la hiérarchie d'héritage à l'exécution :
• isinstance(objet, Classe) : Vérifie si un objet est une instance de Classe ou de l'une de ses sous-classes.
• issubclass(ClasseFille, ClasseMere) : Vérifie la relation d'héritage entre deux types.

PYTHONpython_heritage_simple.py
  1. """
  2. Chapitre 5 - Exemple 2 : L'Héritage Simple en Python
  3. Syntaxe de dérivation, super(), redéfinition et introspection (isinstance / issubclass)
  4. """
  5.  
  6. class Employe:
  7. """Classe de base représentant un employé."""
  8.  
  9. def __init__(self, nom: str, salaire_base: float):
  10. self.nom = nom
  11. self.salaire_base = salaire_base
  12.  
  13. def calculer_salaire(self) -> float:
  14. return self.salaire_base
  15.  
  16. def afficher(self):
  17. print(f"[Employe] {self.nom} | Salaire de base : {self.salaire_base:.2f} €")
  18.  
  19.  
  20. class Manager(Employe):
  21. """Classe dérivée spécialisant Employe avec une prime d'encadrement."""
  22.  
  23. def __init__(self, nom: str, salaire_base: float, prime: float, collaborateurs: int):
  24. # super() appelle automatiquement le constructeur de la classe parente
  25. super().__init__(nom, salaire_base)
  26. self.prime = prime
  27. self.collaborateurs = collaborateurs
  28.  
  29. # Redéfinition (Overriding) de la méthode calculer_salaire
  30. def calculer_salaire(self) -> float:
  31. # Appel de la version parente via super() + ajout de la prime
  32. return super().calculer_salaire() + self.prime
  33.  
  34. # Redéfinition de la méthode d'affichage
  35. def afficher(self):
  36. # Réutilisation du code de la classe mère
  37. super().afficher()
  38. print(f" └── Manager : Prime de {self.prime:.2f} € "
  39. f"({self.collaborateurs} collaborateurs) | Total : {self.calculer_salaire():.2f} €")
  40.  
  41.  
  42. if __name__ == "__main__":
  43. print("=== 2. Héritage Simple & super() en Python ===\n")
  44.  
  45. emp = Employe("Alice Bernard", 2400.0)
  46. emp.afficher()
  47. print()
  48.  
  49. mgr = Manager("Bob Martin", 3800.0, 650.0, 5)
  50. mgr.afficher()
  51.  
  52. print("\n--- Introspection dynamique des types en Python ---")
  53. print("isinstance(mgr, Manager) :", isinstance(mgr, Manager)) # True
  54. print("isinstance(mgr, Employe) :", isinstance(mgr, Employe)) # True (relation EST-UN)
  55. print("issubclass(Manager, Employe) :", issubclass(Manager, Employe)) # True
  56. print("issubclass(Employe, Manager) :", issubclass(Employe, Manager)) # False
3

Le Polymorphisme et le Duck Typing

⏱️ 20 minutes

3.1. Pas de mot-clé virtual en Python !

En C++, le polymorphisme dynamique exige trois conditions préalables :

  1. Une classe de base commune.
  2. Le mot-clé virtual sur les méthodes concernées pour construire une table de pointeurs de fonctions (vtable).
  3. Une manipulation exclusive par pointeurs ou références (Base* ou Base&).

En Python, toutes les méthodes sont virtuelles par défaut ! Le mécanisme de liaison dynamique est inhérent au langage : l'interpréteur résout le nom de la méthode au moment exact de l'appel à l'exécution.

3.2. Le Duck Typing : Le Polymorphisme sans héritage

🦆 Le principe du Duck Typing

« If it walks like a duck and quacks like a duck, it's a duck. »
(Si cela marche comme un canard et cancane comme un canard, alors c'est un canard.)

En Python, une fonction n'exige pas que ses arguments héritent d'une classe de base particulière. Elle s'intéresse uniquement aux méthodes que l'objet est capable d'exécuter. Un Canard, un Chien et un RobotParleur peuvent tous être passés à la même fonction faire_parler() dès lors qu'ils possèdent chacun une méthode emettre_son() !

3.3. Contrats d'interfaces formels avec abc.ABC

Si un projet d'envergure nécessite d'imposer un contrat formel strict (équivalent des classes virtuelles pures en C++ avec virtual void f() = 0;), Python fournit le module standard abc (Abstract Base Classes) :

PYTHONpython_polymorphisme_ducktyping.py
  1. """
  2. Chapitre 5 - Exemple 3 : Le Polymorphisme et le Duck Typing en Python
  3. Comparaison avec C++ : pas de mot-clé 'virtual', typage dynamique et Abstract Base Classes (ABC)
  4. """
  5.  
  6. from abc import ABC, abstractmethod
  7.  
  8. # =====================================================================
  9. # 1. Duck Typing : Le polymorphisme sans aucune classe de base commune !
  10. # =====================================================================
  11.  
  12. class Canard:
  13. def emettre_son(self) -> str:
  14. return "Coin-coin !"
  15.  
  16. class Chien:
  17. def emettre_son(self) -> str:
  18. return "Ouaf-ouaf !"
  19.  
  20. class RobotParleur:
  21. # Cette classe n'a aucun lien biologique avec Canard ou Chien !
  22. def emettre_son(self) -> str:
  23. return "Bip-boup : Synthèse vocale activée."
  24.  
  25. # Fonction polymorphique : elle ne vérifie AUCUN type statique à la compilation !
  26. # Tant que l'objet reçu possède la méthode 'emettre_son()', le code s'exécute.
  27. def faire_parler(entite):
  28. print(f">> {type(entite).__name__} s'exprime : {entite.emettre_son()}")
  29.  
  30.  
  31. # =====================================================================
  32. # 2. Polymorphisme formel avec Classes Abstraites (abc.ABC)
  33. # Équivalent des classes virtuelles pures en C++ (virtual void f() = 0;)
  34. # =====================================================================
  35.  
  36. class FormeGeometrique(ABC):
  37. """Classe abstraite définissant un contrat strict."""
  38.  
  39. @abstractmethod
  40. def aire(self) -> float:
  41. """Méthode abstraite devant impérativement être implémentée par les dérivées."""
  42. pass
  43.  
  44. class Rectangle(FormeGeometrique):
  45. def __init__(self, largeur: float, hauteur: float):
  46. self.largeur = largeur
  47. self.hauteur = hauteur
  48.  
  49. def aire(self) -> float:
  50. return self.largeur * self.hauteur
  51.  
  52. class Cercle(FormeGeometrique):
  53. def __init__(self, rayon: float):
  54. self.rayon = rayon
  55.  
  56. def aire(self) -> float:
  57. import math
  58. return math.pi * (self.rayon ** 2)
  59.  
  60.  
  61. if __name__ == "__main__":
  62. print("=== 3. Polymorphisme & Duck Typing en Python ===\n")
  63.  
  64. print("--- 1. Duck Typing pur (aucun héritage commun requis) ---")
  65. creatures = [Canard(), Chien(), RobotParleur()]
  66. for c in creatures:
  67. faire_parler(c)
  68.  
  69. print("\n--- 2. Classes Abstraites (ABC) et Contrat d'Interface ---")
  70. formes = [Rectangle(4.0, 5.0), Cercle(3.0)]
  71. for f in formes:
  72. print(f"Forme {type(f).__name__} -> Aire = {f.aire():.2f}")
  73.  
  74. # Tentative d'instanciation de la classe abstraite directe :
  75. try:
  76. f_abstraite = FormeGeometrique() # ❌ TypeError
  77. except TypeError as e:
  78. print("\n>> Interdiction d'instancier une ABC :", e)
4

L'Héritage en Diamant & le MRO (Method Resolution Order)

⏱️ 20 minutes

4.1. Le Problème du Diamant revisité

Au Chapitre 2, nous avons étudié le redouté problème du diamant en C++ :
Lorsqu'une classe D hérite de B et C, qui dérivent toutes deux de A, le C++ duplique le sous-objet A en mémoire à moins d'utiliser l'héritage virtuel : virtual public A.

Structure du Diamant en Python
🏛️ Sommet : Appareil
id_serie
↙ hérite de ↘ hérite de
⌨️ Clavier
disposition
🖥️ Ecran
resolution
↘ hérite de ↙ hérite de
📱 TerminalPortable
nom_modele

4.2. La Solution Python : L'algorithme C3 Linearization et le MRO

En Python, l'héritage en diamant fonctionne nativement sans aucun mot-clé spécial !
Pour résoudre les ambiguïtés et garantir qu'aucune classe ancêtre ne soit visitée en double, Python applique l'algorithme C3 Linearization pour calculer un ordre linéaire strict appelé MRO (Method Resolution Order).

Vous pouvez inspecter l'ordre exact calculé par Python pour n'importe quelle classe grâce à l'attribut __mro__ ou la méthode .mro() :

TerminalPortable.mro() : [TerminalPortable, Clavier, Ecran, Appareil, object]
⚡ La règle du super() coopératif

Pour que le diamant fonctionne parfaitement sans appel dupliqué à Appareil.__init__, chaque classe intermédiaire doit appeler super().__init__(**kwargs) en transmettant les arguments résiduels. De cette manière, super() ne pointe pas nécessairement vers le parent direct, mais vers la classe suivante dans la liste du MRO !

PYTHONpython_heritage_diamant_mro.py
  1. """
  2. Chapitre 5 - Exemple 4 : L'Héritage en Diamant & le MRO (Method Resolution Order) en Python
  3. Comment Python résout élégamment le 'Diamond of Death' grâce à l'algorithme C3 et super() coopératif.
  4. """
  5.  
  6. # 1. Sommet du diamant
  7. class Appareil:
  8. def __init__(self, id_serie: int, **kwargs):
  9. super().__init__(**kwargs)
  10. self.id_serie = id_serie
  11. print(f" [1. Appareil] Initialisé une seule fois avec ID: {self.id_serie}")
  12.  
  13. def statut(self):
  14. print(f" -> Statut Appareil #{self.id_serie} : En ligne")
  15.  
  16.  
  17. # 2. Branche gauche
  18. class Clavier(Appareil):
  19. def __init__(self, disposition: str, **kwargs):
  20. super().__init__(**kwargs)
  21. self.disposition = disposition
  22. print(f" [2. Clavier] Initialisé avec disposition: {self.disposition}")
  23.  
  24. def saisir(self):
  25. print(f" -> Saisie sur clavier {self.disposition}")
  26.  
  27.  
  28. # 3. Branche droite
  29. class Ecran(Appareil):
  30. def __init__(self, resolution: str, **kwargs):
  31. super().__init__(**kwargs)
  32. self.resolution = resolution
  33. print(f" [2. Ecran] Initialisé avec résolution: {self.resolution}")
  34.  
  35. def afficher(self):
  36. print(f" -> Affichage {self.resolution}")
  37.  
  38.  
  39. # 4. Pointe du diamant : Héritage Multiple (Clavier, Ecran)
  40. class TerminalPortable(Clavier, Ecran):
  41. def __init__(self, id_serie: int, disposition: str, resolution: str, nom_modele: str):
  42. # En Python, super() coopératif parcourt la chaîne MRO de manière linéaire !
  43. # Les arguments nommés (**kwargs) permettent de distribuer les paramètres à chaque classe.
  44. super().__init__(id_serie=id_serie, disposition=disposition, resolution=resolution)
  45. self.nom_modele = nom_modele
  46. print(f"[3. TerminalPortable] Modèle prêt : \"{self.nom_modele}\"")
  47.  
  48.  
  49. if __name__ == "__main__":
  50. print("=== 4. L'Héritage en Diamant et le MRO en Python ===\n")
  51.  
  52. print("--- 1. Ordre de Résolution des Méthodes (MRO) calculé par Python ---")
  53. # L'algorithme C3 Linearization calcule un ordre linéaire sans ambiguïté :
  54. for i, cls in enumerate(TerminalPortable.mro(), 1):
  55. print(f" Étape {i} : {cls.__name__}")
  56.  
  57. print("\n--- 2. Instanciation de TerminalPortable ---")
  58. terminal = TerminalPortable(
  59. id_serie=98765,
  60. disposition="AZERTY",
  61. resolution="1920x1080",
  62. nom_modele="PadPro-2026"
  63. )
  64.  
  65. print("\n--- 3. Exécution des méthodes héritées ---")
  66. terminal.saisir()
  67. terminal.afficher()
  68. # Aucun conflit d'ambiguïté pour statut() : l'ancêtre commun est unique et résolu par le MRO
  69. terminal.statut()
  70.  
  71. print("\nConclusion : En Python, PAS BESOIN de mot-clé 'virtual' !")
  72. print("Le MRO (C3 Linearization) garantit qu'Appareil n'est visité qu'une seule fois.")
5

Ce qui est Possible vs Non Possible en Python par rapport au C++

⏱️ 15 minutes

Pour un développeur venant du monde C++, Python offre une souplesse extraordinaire mais impose d'abandonner certains automatismes du compilateur statique :

Fonctionnalité POO Statut en C++ Statut en Python Explication & Équivalent Python
Héritage en Diamant Exige virtual public ✅ POSSIBLE Géré nativement et automatiquement via l'algorithme de linéarisation C3 (MRO).
Duck Typing (Polymorphisme libre) Exige Concepts ou Templates ✅ POSSIBLE Totalement naturel : aucun héritage commun requis pour invoquer une même méthode.
Surcharge d'Opérateurs Opérateurs operator+, [] ✅ POSSIBLE Méthodes spéciales (*dunder*) très simples : __add__, __eq__, __getitem__.
Surcharge de Méthodes par signature Native à la compilation ❌ IMPOSSIBLE La dernière définition d'une fonction écrase la précédente. On utilise des arguments par défaut ou *args.
Encapsulation private stricte Inviolable par le compilateur ❌ IMPOSSIBLE Pas de frontière étanche : le Name Mangling (__var) est un simple renommage contournable.
Destruction Déterministe (RAII) Instantanée à la sortie du bloc ❌ IMPOSSIBLE Le ramasse-miettes (GC) est asynchrone ; __del__ n'est pas fiable. On utilise l'instruction with.

Démonstration pratique des différences

PYTHONpython_ce_qui_est_possible_ou_pas.py
  1. """
  2. Chapitre 5 - Exemple 5 : Ce qui est POSSIBLE et NON POSSIBLE en Python vs C++
  3. Surcharge de méthodes, surcharge d'opérateurs (dunder), et gestion des ressources
  4. """
  5.  
  6. # =====================================================================
  7. # 1. ❌ NON POSSIBLE : La Surcharge de méthodes par signature
  8. # En C++, on peut écrire : void afficher(int); void afficher(string);
  9. # En Python, la deuxième définition ÉCRASE silencieusement la première !
  10. # =====================================================================
  11.  
  12. class Calculatrice:
  13. # Première définition
  14. def additionner(self, a: int, b: int):
  15. return a + b
  16.  
  17. # Seconde définition : ELLE ÉCRASE LA PRÉCÉDENTE !
  18. def additionner(self, a: int, b: int, c: int):
  19. return a + b + c
  20.  
  21. # ✅ SOLUTION PYTHONIQUE : Arguments optionnels avec valeurs par défaut
  22. def additionner_pythonique(self, a: int, b: int, c: int = 0):
  23. return a + b + c
  24.  
  25.  
  26. # =====================================================================
  27. # 2. ✅ POSSIBLE & ÉLÉGANT : Surcharge d'opérateurs avec les méthodes magiques
  28. # Python permet de surcharger +, ==, <, len(), [] via __add__, __eq__, etc.
  29. # =====================================================================
  30.  
  31. class Vecteur2D:
  32. def __init__(self, x: float, y: float):
  33. self.x = x
  34. self.y = y
  35.  
  36. # Surcharge de l'opérateur '+'
  37. def __add__(self, autre: "Vecteur2D") -> "Vecteur2D":
  38. return Vecteur2D(self.x + autre.x, self.y + autre.y)
  39.  
  40. # Surcharge de l'opérateur '=='
  41. def __eq__(self, autre: object) -> bool:
  42. if not isinstance(autre, Vecteur2D):
  43. return False
  44. return self.x == autre.x and self.y == autre.y
  45.  
  46. # Surcharge de l'opérateur '*' par un scalaire
  47. def __mul__(self, scalaire: float) -> "Vecteur2D":
  48. return Vecteur2D(self.x * scalaire, self.y * scalaire)
  49.  
  50. def __str__(self) -> str:
  51. return f"Vecteur2D({self.x}, {self.y})"
  52.  
  53.  
  54. # =====================================================================
  55. # 3. ❌ NON POSSIBLE : Le RAII déterministe de C++ (Destructeur instantané)
  56. # En Python, __del__ dépend du Garbage Collector et n'est pas fiable.
  57. # ✅ SOLUTION PYTHONIQUE : Le gestionnaire de contexte (instruction 'with')
  58. # =====================================================================
  59.  
  60. class RessourceFichier:
  61. """Simulation d'une ressource fermée proprement avec le protocole 'with'."""
  62.  
  63. def __init__(self, nom: str):
  64. self.nom = nom
  65.  
  66. def __enter__(self):
  67. print(f" [+] Ouverture sécurisée de '{self.nom}' (__enter__)")
  68. return self
  69.  
  70. def __exit__(self, exc_type, exc_val, exc_tb):
  71. print(f" [-] Fermeture garantie de '{self.nom}' (__exit__)")
  72. # Renvoyer False pour propager les exceptions éventuelles
  73.  
  74.  
  75. if __name__ == "__main__":
  76. print("=== 5. Ce qui est Possible ou Non Possible en Python vs C++ ===\n")
  77.  
  78. # 1. Démonstration de l'écrasement de signature
  79. print("--- 1. Échec de la surcharge classique par signature ---")
  80. calc = Calculatrice()
  81. try:
  82. # Appel avec 2 arguments : échoue car la version à 3 arguments a écrasé la première !
  83. calc.additionner(10, 20)
  84. except TypeError as e:
  85. print(">> Erreur TypeError attendue :", e)
  86.  
  87. # Version pythonique avec argument par défaut :
  88. print("Version pythonique (2 args) :", calc.additionner_pythonique(10, 20))
  89. print("Version pythonique (3 args) :", calc.additionner_pythonique(10, 20, 30))
  90.  
  91. # 2. Surcharge d'opérateurs
  92. print("\n--- 2. Surcharge d'opérateurs simplifiée (__add__, __mul__, __eq__) ---")
  93. v1 = Vecteur2D(2.0, 3.0)
  94. v2 = Vecteur2D(4.0, 1.0)
  95. v3 = v1 + v2 # Invoque v1.__add__(v2)
  96. v4 = v1 * 3.0 # Invoque v1.__mul__(3.0)
  97.  
  98. print("v1 + v2 =", v3)
  99. print("v1 * 3 =", v4)
  100. print("v3 == Vecteur2D(6.0, 4.0) :", v3 == Vecteur2D(6.0, 4.0)) # True via __eq__
  101.  
  102. # 3. Gestionnaire de contexte (With)
  103. print("\n--- 3. Gestion des ressources : with vs RAII C++ ---")
  104. with RessourceFichier("donnees_clients.csv") as ressource:
  105. print(" -> Traitement en cours dans le bloc protégé...")
  106. print("Bloc 'with' terminé (ressource déjà fermée proprement).")
6

Synthèse & Auto-évaluation

⏱️ 10 minutes

6.1. Fiche Mémo Récapitulative

Concept Mise en œuvre en Python Point clé à retenir
Instance courante self obligatoire en 1er paramètre Explicite en Python, implicite (this) en C++.
Constructeur def __init__(self, ...) Initialise les attributs dynamiquement sur l'objet.
Accesseurs / Mutateurs @property et @nom.setter Syntaxe d'attribut naturelle avec logique de validation sous-jacente.
Polymorphisme Duck Typing dynamique Pas besoin de classe de base commune ni de mot-clé virtual.
Diamant d'héritage MRO (Method Resolution Order) Linéarisation C3 automatique sans duplication de sous-objet.
Surcharge de méthodes Arguments par défaut ou *args Deux méthodes du même nom dans une classe s'écrasent.

6.2. QCM de Réflexion (Niveau L3)

❓ Question 1 : Si vous déclarez deux méthodes def calculer(self, a): et def calculer(self, a, b): au sein de la même classe en Python, que se produit-il ?
A. Python sélectionne automatiquement la bonne méthode selon le nombre d'arguments fournis (surcharge par signature comme en C++).
B. La seconde définition écrase purement et simplement la première dans le dictionnaire de la classe ; appeler calculer avec un seul argument lèvera une exception TypeError.
C. Le compilateur Python refuse d'exécuter le script et émet une erreur de syntaxe.
Réponse B : En Python, une classe est un dictionnaire dynamique de noms. Définir une seconde fonction calculer remplace simplement la référence vers la première fonction. Pour simuler une surcharge, on utilise des paramètres par défaut : def calculer(self, a, b=None):.
❓ Question 2 : Comment Python garantit-il qu'une classe de base au sommet d'un diamant d'héritage n'est pas initialisée deux fois ?
A. En exigeant d'écrire class B(virtual A): comme en C++.
B. Grâce à l'algorithme de linéarisation C3 qui calcule un ordre unique de résolution des méthodes (MRO) parcouru de manière coopérative par super().
C. Python interdit l'héritage multiple dès qu'un diamant est détecté.
Réponse B : L'algorithme C3 MRO aplatit la hiérarchie en diamant en une séquence linéaire ordonnée (ex: [D, B, C, A, object]). Chaque classe invoque super(), assurant que l'ancêtre A n'est visité qu'une seule et unique fois.
❓ Question 3 : En Python, un attribut nommé __secret est-il physiquement protégé contre toute lecture depuis l'extérieur de la classe ?
A. Oui, l'accès direct est strictement verrouillé au niveau de la machine virtuelle et personne ne peut y accéder.
B. Non, Python applique simplement le Name Mangling en le renommant _NomClasse__secret, accessible par quiconque connaît cette règle.
C. Oui, à condition d'avoir importé le module private.
Réponse B : En Python, il n'existe pas d'encapsulation dure inviolable. Le double tiret bas prévient les collisions de noms involontaires lors de l'héritage, mais l'attribut reste lisible via objet._NomClasse__secret.

📝 Mini-Exercice de mise en pratique

Concevez en Python une hiérarchie de gestion de formes géométriques :

  • 1. Classe abstraite Forme(ABC) avec une méthode abstraite perimetre().
  • 2. Classe Carre(Forme) avec une @property pour le côté garantissant qu'il est strictement positif.
  • 3. Surchargez la méthode magique __eq__ dans Carre pour considérer deux carrés égaux s'ils ont le même côté.
  • 4. Écrivez une fonction polymorphique afficher_perimetre(f) exploitant le Duck Typing.

Navigation dans le cours

Félicitations, vous avez exploré la POO en C++ et sa transposition dynamique en Python !

← Revoir Chapitre 4 (La STL) Comparer avec Chapitre 2 (Héritage C++) ➔ Consulter l'Annexe C++ ➔ Accueil du Cours ➔