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
selfet 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é
virtualn'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).
Classes, Instances et Encapsulation en Python
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.
def __init__(self, ...): Le constructeur d'initialisation de l'objet (appelé automatiquement lors de l'instanciationobj = MaClasse(...)).self.attribut = valeur: Crée ou modifie dynamiquement un attribut attaché à l'instance courante.
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 :
""" Chapitre 5 - Exemple 1 : Classes, Constructeur et Encapsulation en Python Notion de self, convention de nommage, name mangling et décorateur @property """ class CompteBancaire: """Modélisation d'un compte bancaire avec validation des invariants.""" def __init__(self, titulaire: str, solde_initial: float = 0.0): # Attribut public conventionnel self.titulaire = titulaire # Attribut "protégé" par convention (un seul tiret du bas : _solde) # Indique aux autres développeurs : "usage interne, ne pas modifier directement" self._solde = 0.0 # Attribut "privé" avec Name Mangling (deux tirets du bas : __numero_compte) # Python renomme automatiquement l'attribut en _CompteBancaire__numero_compte self.__numero_compte = f"FR76-{abs(hash(titulaire)) % 100000:05d}" # Utilisation du setter pour garantir l'invariant (solde positif ou nul) self.solde = solde_initial # Propriété (Getter) : accès naturel 'compte.solde' @property def solde(self) -> float: return self._solde # Mutateur (Setter) avec contrôle de validité @solde.setter def solde(self, nouveau_solde: float): if nouveau_solde < 0: raise ValueError(f"Erreur : Le solde ne peut pas être négatif ({nouveau_solde} € demandé).") self._solde = nouveau_solde @property def numero_compte(self) -> str: return self.__numero_compte def deposer(self, montant: float): if montant <= 0: raise ValueError("Le montant déposé doit être strictement positif.") self._solde += montant print(f"[Dépôt] +{montant:.2f} € sur le compte de {self.titulaire}") # Méthodes spéciales (Dunder methods) pour représentation textuelle def __str__(self) -> str: return f"Compte de {self.titulaire} ({self.__numero_compte}) : {self._solde:.2f} €" def __repr__(self) -> str: return f"CompteBancaire(titulaire='{self.titulaire}', solde={self._solde})" if __name__ == "__main__": print("=== 1. Classes & Encapsulation en Python ===\n") compte = CompteBancaire("Claire Dupont", 1500.0) print("Affichage via __str__ :", compte) print("Numéro de compte (lecture seule) :", compte.numero_compte) # Dépôt compte.deposer(250.0) print("Nouveau solde via @property :", compte.solde, "€") # Modification via setter avec validation compte.solde = 2000.0 print("Solde après mise à jour :", compte.solde, "€") # Tentative d'affectation invalide try: compte.solde = -500.0 except ValueError as e: print(">> Validation d'invariant réussie :", e) # DÉMONSTRATION DU "CONSENTING ADULTS" EN PYTHON : # En Python, le mot-clé 'private' de C++ n'existe pas ! # L'attribut à double tiret __numero_compte est simplement "masqué" par Name Mangling : print("\n--- Analyse du 'Name Mangling' en Python ---") print("Accès direct à compte.__numero_compte...") try: print(compte.__numero_compte) except AttributeError as e: print(">> AttributeError déclenché :", e) # Mais l'attribut est toujours accessible via son nom altéré par Python : nom_altere = compte._CompteBancaire__numero_compte print(">> Contournement via '_CompteBancaire__numero_compte' :", nom_altere) print("En Python : 'We are all consenting adults here' (l'encapsulation est une convention).")
L'Héritage Simple et la Fonction super()
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__.
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.
""" Chapitre 5 - Exemple 2 : L'Héritage Simple en Python Syntaxe de dérivation, super(), redéfinition et introspection (isinstance / issubclass) """ class Employe: """Classe de base représentant un employé.""" def __init__(self, nom: str, salaire_base: float): self.nom = nom self.salaire_base = salaire_base def calculer_salaire(self) -> float: return self.salaire_base def afficher(self): print(f"[Employe] {self.nom} | Salaire de base : {self.salaire_base:.2f} €") class Manager(Employe): """Classe dérivée spécialisant Employe avec une prime d'encadrement.""" def __init__(self, nom: str, salaire_base: float, prime: float, collaborateurs: int): # super() appelle automatiquement le constructeur de la classe parente super().__init__(nom, salaire_base) self.prime = prime self.collaborateurs = collaborateurs # Redéfinition (Overriding) de la méthode calculer_salaire def calculer_salaire(self) -> float: # Appel de la version parente via super() + ajout de la prime return super().calculer_salaire() + self.prime # Redéfinition de la méthode d'affichage def afficher(self): # Réutilisation du code de la classe mère super().afficher() print(f" └── Manager : Prime de {self.prime:.2f} € " f"({self.collaborateurs} collaborateurs) | Total : {self.calculer_salaire():.2f} €") if __name__ == "__main__": print("=== 2. Héritage Simple & super() en Python ===\n") emp = Employe("Alice Bernard", 2400.0) emp.afficher() print() mgr = Manager("Bob Martin", 3800.0, 650.0, 5) mgr.afficher() print("\n--- Introspection dynamique des types en Python ---") print("isinstance(mgr, Manager) :", isinstance(mgr, Manager)) # True print("isinstance(mgr, Employe) :", isinstance(mgr, Employe)) # True (relation EST-UN) print("issubclass(Manager, Employe) :", issubclass(Manager, Employe)) # True print("issubclass(Employe, Manager) :", issubclass(Employe, Manager)) # False
Le Polymorphisme et le Duck Typing
3.1. Pas de mot-clé virtual en Python !
En C++, le polymorphisme dynamique exige trois conditions préalables :
- Une classe de base commune.
- Le mot-clé
virtualsur les méthodes concernées pour construire une table de pointeurs de fonctions (vtable). - Une manipulation exclusive par pointeurs ou références (
Base*ouBase&).
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
« 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) :
""" Chapitre 5 - Exemple 3 : Le Polymorphisme et le Duck Typing en Python Comparaison avec C++ : pas de mot-clé 'virtual', typage dynamique et Abstract Base Classes (ABC) """ from abc import ABC, abstractmethod # ===================================================================== # 1. Duck Typing : Le polymorphisme sans aucune classe de base commune ! # ===================================================================== class Canard: def emettre_son(self) -> str: return "Coin-coin !" class Chien: def emettre_son(self) -> str: return "Ouaf-ouaf !" class RobotParleur: # Cette classe n'a aucun lien biologique avec Canard ou Chien ! def emettre_son(self) -> str: return "Bip-boup : Synthèse vocale activée." # Fonction polymorphique : elle ne vérifie AUCUN type statique à la compilation ! # Tant que l'objet reçu possède la méthode 'emettre_son()', le code s'exécute. def faire_parler(entite): print(f">> {type(entite).__name__} s'exprime : {entite.emettre_son()}") # ===================================================================== # 2. Polymorphisme formel avec Classes Abstraites (abc.ABC) # Équivalent des classes virtuelles pures en C++ (virtual void f() = 0;) # ===================================================================== class FormeGeometrique(ABC): """Classe abstraite définissant un contrat strict.""" @abstractmethod def aire(self) -> float: """Méthode abstraite devant impérativement être implémentée par les dérivées.""" pass class Rectangle(FormeGeometrique): def __init__(self, largeur: float, hauteur: float): self.largeur = largeur self.hauteur = hauteur def aire(self) -> float: return self.largeur * self.hauteur class Cercle(FormeGeometrique): def __init__(self, rayon: float): self.rayon = rayon def aire(self) -> float: import math return math.pi * (self.rayon ** 2) if __name__ == "__main__": print("=== 3. Polymorphisme & Duck Typing en Python ===\n") print("--- 1. Duck Typing pur (aucun héritage commun requis) ---") creatures = [Canard(), Chien(), RobotParleur()] for c in creatures: faire_parler(c) print("\n--- 2. Classes Abstraites (ABC) et Contrat d'Interface ---") formes = [Rectangle(4.0, 5.0), Cercle(3.0)] for f in formes: print(f"Forme {type(f).__name__} -> Aire = {f.aire():.2f}") # Tentative d'instanciation de la classe abstraite directe : try: f_abstraite = FormeGeometrique() # ❌ TypeError except TypeError as e: print("\n>> Interdiction d'instancier une ABC :", e)
L'Héritage en Diamant & le MRO (Method Resolution Order)
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.
id_serie
disposition
resolution
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() :
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 !
""" Chapitre 5 - Exemple 4 : L'Héritage en Diamant & le MRO (Method Resolution Order) en Python Comment Python résout élégamment le 'Diamond of Death' grâce à l'algorithme C3 et super() coopératif. """ # 1. Sommet du diamant class Appareil: def __init__(self, id_serie: int, **kwargs): super().__init__(**kwargs) self.id_serie = id_serie print(f" [1. Appareil] Initialisé une seule fois avec ID: {self.id_serie}") def statut(self): print(f" -> Statut Appareil #{self.id_serie} : En ligne") # 2. Branche gauche class Clavier(Appareil): def __init__(self, disposition: str, **kwargs): super().__init__(**kwargs) self.disposition = disposition print(f" [2. Clavier] Initialisé avec disposition: {self.disposition}") def saisir(self): print(f" -> Saisie sur clavier {self.disposition}") # 3. Branche droite class Ecran(Appareil): def __init__(self, resolution: str, **kwargs): super().__init__(**kwargs) self.resolution = resolution print(f" [2. Ecran] Initialisé avec résolution: {self.resolution}") def afficher(self): print(f" -> Affichage {self.resolution}") # 4. Pointe du diamant : Héritage Multiple (Clavier, Ecran) class TerminalPortable(Clavier, Ecran): def __init__(self, id_serie: int, disposition: str, resolution: str, nom_modele: str): # En Python, super() coopératif parcourt la chaîne MRO de manière linéaire ! # Les arguments nommés (**kwargs) permettent de distribuer les paramètres à chaque classe. super().__init__(id_serie=id_serie, disposition=disposition, resolution=resolution) self.nom_modele = nom_modele print(f"[3. TerminalPortable] Modèle prêt : \"{self.nom_modele}\"") if __name__ == "__main__": print("=== 4. L'Héritage en Diamant et le MRO en Python ===\n") print("--- 1. Ordre de Résolution des Méthodes (MRO) calculé par Python ---") # L'algorithme C3 Linearization calcule un ordre linéaire sans ambiguïté : for i, cls in enumerate(TerminalPortable.mro(), 1): print(f" Étape {i} : {cls.__name__}") print("\n--- 2. Instanciation de TerminalPortable ---") terminal = TerminalPortable( id_serie=98765, disposition="AZERTY", resolution="1920x1080", nom_modele="PadPro-2026" ) print("\n--- 3. Exécution des méthodes héritées ---") terminal.saisir() terminal.afficher() # Aucun conflit d'ambiguïté pour statut() : l'ancêtre commun est unique et résolu par le MRO terminal.statut() print("\nConclusion : En Python, PAS BESOIN de mot-clé 'virtual' !") print("Le MRO (C3 Linearization) garantit qu'Appareil n'est visité qu'une seule fois.")
Ce qui est Possible vs Non Possible en Python par rapport au C++
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
""" Chapitre 5 - Exemple 5 : Ce qui est POSSIBLE et NON POSSIBLE en Python vs C++ Surcharge de méthodes, surcharge d'opérateurs (dunder), et gestion des ressources """ # ===================================================================== # 1. ❌ NON POSSIBLE : La Surcharge de méthodes par signature # En C++, on peut écrire : void afficher(int); void afficher(string); # En Python, la deuxième définition ÉCRASE silencieusement la première ! # ===================================================================== class Calculatrice: # Première définition def additionner(self, a: int, b: int): return a + b # Seconde définition : ELLE ÉCRASE LA PRÉCÉDENTE ! def additionner(self, a: int, b: int, c: int): return a + b + c # ✅ SOLUTION PYTHONIQUE : Arguments optionnels avec valeurs par défaut def additionner_pythonique(self, a: int, b: int, c: int = 0): return a + b + c # ===================================================================== # 2. ✅ POSSIBLE & ÉLÉGANT : Surcharge d'opérateurs avec les méthodes magiques # Python permet de surcharger +, ==, <, len(), [] via __add__, __eq__, etc. # ===================================================================== class Vecteur2D: def __init__(self, x: float, y: float): self.x = x self.y = y # Surcharge de l'opérateur '+' def __add__(self, autre: "Vecteur2D") -> "Vecteur2D": return Vecteur2D(self.x + autre.x, self.y + autre.y) # Surcharge de l'opérateur '==' def __eq__(self, autre: object) -> bool: if not isinstance(autre, Vecteur2D): return False return self.x == autre.x and self.y == autre.y # Surcharge de l'opérateur '*' par un scalaire def __mul__(self, scalaire: float) -> "Vecteur2D": return Vecteur2D(self.x * scalaire, self.y * scalaire) def __str__(self) -> str: return f"Vecteur2D({self.x}, {self.y})" # ===================================================================== # 3. ❌ NON POSSIBLE : Le RAII déterministe de C++ (Destructeur instantané) # En Python, __del__ dépend du Garbage Collector et n'est pas fiable. # ✅ SOLUTION PYTHONIQUE : Le gestionnaire de contexte (instruction 'with') # ===================================================================== class RessourceFichier: """Simulation d'une ressource fermée proprement avec le protocole 'with'.""" def __init__(self, nom: str): self.nom = nom def __enter__(self): print(f" [+] Ouverture sécurisée de '{self.nom}' (__enter__)") return self def __exit__(self, exc_type, exc_val, exc_tb): print(f" [-] Fermeture garantie de '{self.nom}' (__exit__)") # Renvoyer False pour propager les exceptions éventuelles if __name__ == "__main__": print("=== 5. Ce qui est Possible ou Non Possible en Python vs C++ ===\n") # 1. Démonstration de l'écrasement de signature print("--- 1. Échec de la surcharge classique par signature ---") calc = Calculatrice() try: # Appel avec 2 arguments : échoue car la version à 3 arguments a écrasé la première ! calc.additionner(10, 20) except TypeError as e: print(">> Erreur TypeError attendue :", e) # Version pythonique avec argument par défaut : print("Version pythonique (2 args) :", calc.additionner_pythonique(10, 20)) print("Version pythonique (3 args) :", calc.additionner_pythonique(10, 20, 30)) # 2. Surcharge d'opérateurs print("\n--- 2. Surcharge d'opérateurs simplifiée (__add__, __mul__, __eq__) ---") v1 = Vecteur2D(2.0, 3.0) v2 = Vecteur2D(4.0, 1.0) v3 = v1 + v2 # Invoque v1.__add__(v2) v4 = v1 * 3.0 # Invoque v1.__mul__(3.0) print("v1 + v2 =", v3) print("v1 * 3 =", v4) print("v3 == Vecteur2D(6.0, 4.0) :", v3 == Vecteur2D(6.0, 4.0)) # True via __eq__ # 3. Gestionnaire de contexte (With) print("\n--- 3. Gestion des ressources : with vs RAII C++ ---") with RessourceFichier("donnees_clients.csv") as ressource: print(" -> Traitement en cours dans le bloc protégé...") print("Bloc 'with' terminé (ressource déjà fermée proprement).")
Synthèse & Auto-évaluation
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)
def calculer(self, a): et def calculer(self, a, b): au sein de la même classe en Python, que se produit-il ?
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):.
[D, B, C, A, object]). Chaque classe invoque super(), assurant que l'ancêtre A n'est visité qu'une seule et unique fois.
__secret est-il physiquement protégé contre toute lecture depuis l'extérieur de la classe ?
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 abstraiteperimetre(). - 2. Classe
Carre(Forme)avec une@propertypour le côté garantissant qu'il est strictement positif. - 3. Surchargez la méthode magique
__eq__dansCarrepour 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 !