
La POO avancée en Python regroupe les outils qui vont au-delà des classes de base : les dataclass pour écrire moins de code, les classes abstraites (abc) pour imposer un contrat, l'héritage multiple et le MRO, les mixins, la surcharge d'opérateurs, __slots__ et les métaclasses. Ces mécanismes servent à structurer des applications réelles, réduire la duplication et rendre vos objets prévisibles.
Ce guide s'adresse aux développeurs intermédiaires et avancés qui maîtrisent déjà les bases de la POO (classes, objets, héritage simple, self) et veulent monter d'un cran.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Réduire le code répétitif avec les
@dataclass - Imposer un contrat avec les classes abstraites du module
abc - Comprendre le MRO et maîtriser l'héritage multiple avec
super() - Composer des comportements avec les mixins
- Surcharger les opérateurs (
+,==,<) via les méthodes spéciales - Optimiser la mémoire avec
__slots__et découvrir les métaclasses
Réduire le code avec les dataclasses
Section intitulée « Réduire le code avec les dataclasses »Écrire une classe qui ne fait que stocker des données impose de répéter __init__, __repr__ et __eq__. Le décorateur @dataclass (module dataclasses, intégré depuis Python 3.7) génère tout cela pour vous à partir des annotations de type.
from dataclasses import dataclass
@dataclassclass Point: x: int y: int = 0 # valeur par défaut
p = Point(3, 4)print(p) # Point(x=3, y=4) -> __repr__ généréprint(p == Point(3, 4)) # True -> __eq__ généréUne seule déclaration remplace une vingtaine de lignes. Pour un attribut mutable par défaut (liste, dictionnaire), utilisez field(default_factory=...) au lieu d'une valeur directe, afin d'éviter le partage entre instances.
from dataclasses import dataclass, field
@dataclassclass Panier: articles: list = field(default_factory=list)
a, b = Panier(), Panier()a.articles.append("pain")print(a.articles, b.articles) # ['pain'] [] -> pas de partageImposer un contrat avec les classes abstraites
Section intitulée « Imposer un contrat avec les classes abstraites »Une classe abstraite définit des méthodes que les classes filles doivent implémenter, sans fournir elle-même de corps utile. C'est le moyen d'exprimer une interface en Python, grâce au module abc (Abstract Base Classes).
from abc import ABC, abstractmethod
class Forme(ABC): @abstractmethod def aire(self) -> float: ...
class Cercle(Forme): def __init__(self, rayon): self.rayon = rayon
def aire(self) -> float: return 3.14159 * self.rayon ** 2
print(Cercle(2).aire()) # 12.56636Tant qu'une classe fille n'implémente pas toutes les méthodes abstraites, Python refuse de l'instancier :
# Forme() # TypeError: Can't instantiate abstract class Forme# # with abstract method aireLes classes abstraites documentent le contrat attendu et font échouer tôt (à l'instanciation) plutôt que tard (à l'appel d'une méthode manquante).
Héritage multiple et MRO
Section intitulée « Héritage multiple et MRO »Python autorise l'héritage multiple : une classe peut hériter de plusieurs parents. Pour savoir quelle méthode est appelée, Python suit le MRO (Method Resolution Order), l'ordre de résolution des méthodes, calculé par l'algorithme C3.
class A: def salut(self): return "A"
class B(A): def salut(self): return "B -> " + super().salut()
class C(A): def salut(self): return "C -> " + super().salut()
class D(B, C): def salut(self): return "D -> " + super().salut()
print(D().salut()) # D -> B -> C -> Aprint([cls.__name__ for cls in D.__mro__]) # ['D', 'B', 'C', 'A', 'object']Le point clé : super() ne signifie pas « la classe parente » mais « la suivante dans le MRO ». C'est ce qui permet à D de traverser B puis C puis A sans appeler A deux fois. Consultez toujours Classe.__mro__ en cas de doute sur l'ordre.
Composer des comportements avec les mixins
Section intitulée « Composer des comportements avec les mixins »Un mixin est une petite classe qui ajoute un comportement précis, destinée à être combinée avec d'autres par héritage multiple. Elle ne s'utilise jamais seule et ne définit pas d'état complet.
from dataclasses import dataclassimport json
class JSONMixin: def to_json(self) -> str: return json.dumps(self.__dict__)
@dataclassclass Utilisateur(JSONMixin): nom: str age: int
print(Utilisateur("Alice", 30).to_json()) # {"nom": "Alice", "age": 30}Les mixins modularisent les fonctionnalités : sérialisation, comparaison, journalisation. Vous composez une classe en assemblant les briques dont elle a besoin, sans dupliquer le code.
Surcharger les opérateurs
Section intitulée « Surcharger les opérateurs »La surcharge d'opérateurs définit le comportement de +, ==, <, etc. pour vos objets, via des méthodes spéciales. Vos classes s'intègrent alors naturellement au langage.
class Vecteur: def __init__(self, x, y): self.x, self.y = x, y
def __add__(self, autre): return Vecteur(self.x + autre.x, self.y + autre.y)
def __eq__(self, autre): return (self.x, self.y) == (autre.x, autre.y)
def __repr__(self): return f"Vecteur({self.x}, {self.y})"
print(Vecteur(1, 2) + Vecteur(3, 4)) # Vecteur(4, 6)print(Vecteur(1, 2) == Vecteur(1, 2)) # TrueChaque opérateur correspond à une méthode : __add__ pour +, __sub__ pour -, __lt__ pour <, __mul__ pour *. Utilisez cette possibilité avec parcimonie, seulement quand l'opération a un sens intuitif pour l'objet.
Propriétés avancées avec @property
Section intitulée « Propriétés avancées avec @property »Le décorateur @property transforme une méthode en attribut. Au-delà du simple getter, il permet des attributs calculés et une validation à l'écriture, sans changer l'interface publique.
class Temperature: def __init__(self, celsius): self.celsius = celsius # passe par le setter
@property def celsius(self): return self._celsius
@celsius.setter def celsius(self, valeur): if valeur < -273.15: raise ValueError("En dessous du zéro absolu") self._celsius = valeur
@property def fahrenheit(self): # propriété calculée, en lecture seule return self._celsius * 9 / 5 + 32
t = Temperature(25)print(t.fahrenheit) # 77.0t.celsius = 30 # validé par le setterprint(t.fahrenheit) # 86.0L'utilisateur écrit t.celsius = 30 comme un attribut normal, mais la validation s'applique. fahrenheit est recalculé à chaque lecture : aucune donnée redondante à maintenir.
Optimiser la mémoire avec slots
Section intitulée « Optimiser la mémoire avec slots »Par défaut, chaque instance stocke ses attributs dans un dictionnaire __dict__, souple mais coûteux en mémoire. __slots__ fige la liste des attributs autorisés et supprime ce dictionnaire, ce qui réduit l'empreinte mémoire et accélère l'accès, utile quand vous créez des millions d'objets.
class PointOptimise: __slots__ = ("x", "y")
def __init__(self, x, y): self.x, self.y = x, y
p = PointOptimise(1, 2)print(p.x, p.y) # 1 2# p.z = 3 # AttributeError: 'PointOptimise' object has no attribute 'z'En contrepartie, vous ne pouvez plus ajouter d'attribut hors de la liste __slots__. C'est une optimisation ciblée, pas un réflexe systématique.
Aller plus loin : les métaclasses
Section intitulée « Aller plus loin : les métaclasses »Une métaclasse est la classe d'une classe : elle contrôle comment les classes elles-mêmes sont créées. C'est un outil puissant mais rarement nécessaire, réservé aux frameworks (ORM, validation de schémas). L'exemple classique est le singleton, qui garantit une instance unique.
class Singleton(type): _instances = {}
def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super().__call__(*args, **kwargs) return cls._instances[cls]
class Config(metaclass=Singleton): pass
print(Config() is Config()) # True : toujours la même instanceÀ retenir
Section intitulée « À retenir »@dataclassgénère__init__,__repr__et__eq__;field(default_factory=...)pour les défauts mutables,frozen=Truepour l'immuabilité.- Les classes abstraites (
abc.ABC+@abstractmethod) imposent un contrat et empêchent l'instanciation tant qu'il n'est pas rempli. super()suit le MRO, pas la classe parente : inspectezClasse.__mro__en héritage multiple.- Un mixin ajoute un comportement ciblé, à combiner par héritage, jamais utilisé seul.
- La surcharge d'opérateurs (
__add__,__eq__,__lt__) intègre vos objets au langage, à réserver aux cas intuitifs. @propertyoffre attributs calculés et validation ;__slots__réduit la mémoire au prix de la souplesse.- Les métaclasses contrôlent la création des classes : puissantes, mais à éviter sauf besoin réel.
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Les réponses courtes ci-dessous couvrent les questions les plus recherchées sur la POO avancée en Python. Chaque exemple est autonome et testé sur Python 3.12.
@dataclass (module dataclasses, intégré depuis Python 3.7) qui génère automatiquement __init__, __repr__ et __eq__ à partir des annotations de type :from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int = 0
print(Point(3, 4)) # Point(x=3, y=4)
print(Point(3) == Point(3)) # True
Pour un défaut mutable, utilisez field(default_factory=list) ; pour l'immuabilité, @dataclass(frozen=True).abc.ABC et déclare des méthodes @abstractmethod que les classes filles doivent implémenter :from abc import ABC, abstractmethod
class Forme(ABC):
@abstractmethod
def aire(self) -> float: ...
class Cercle(Forme):
def __init__(self, r): self.r = r
def aire(self): return 3.14159 * self.r ** 2
Python refuse d'instancier une classe tant que toutes ses méthodes abstraites ne sont pas définies (Forme() lève TypeError). C'est la façon d'imposer une interface.class A: ...
class B(A): ...
class C(A): ...
class D(B, C): ...
print([c.__name__ for c in D.__mro__])
# ['D', 'B', 'C', 'A', 'object']
Point clé : en héritage multiple, super() suit ce MRO, pas simplement la classe parente. Inspectez Classe.__mro__ en cas de doute.import json
class JSONMixin:
def to_json(self):
return json.dumps(self.__dict__)
class Utilisateur(JSONMixin):
def __init__(self, nom):
self.nom = nom
print(Utilisateur("Alice").to_json()) # {"nom": "Alice"}
Il ne s'utilise jamais seul et ne porte pas d'état complet : il sert à modulariser et réutiliser des fonctionnalités.__slots__ fige la liste des attributs autorisés et supprime le dictionnaire __dict__ de chaque instance, ce qui réduit la mémoire et accélère l'accès :class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x, self.y = x, y
p = Point(1, 2)
# p.z = 3 # AttributeError : attribut non déclaré
Utile quand vous créez des millions d'objets. En contrepartie, vous ne pouvez plus ajouter d'attribut hors de la liste __slots__ : c'est une optimisation ciblée.metaclass= :class Singleton(type):
_instances = {}
def __call__(cls, *a, **k):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*a, **k)
return cls._instances[cls]
class Config(metaclass=Singleton):
pass
print(Config() is Config()) # True
Puissante mais rarement nécessaire : un décorateur de classe suffit le plus souvent. Réservez-la aux frameworks (ORM, validation).