Classes abstraites de base en Python (ABC)
Apprenez à utiliser les classes abstraites Python (ABC) pour imposer une interface commune aux sous-classes via @abstractmethod, les propriétés abstraites et le module abc.
Une classe abstraite est une classe qui ne peut pas être instanciée directement. Elle existe uniquement pour définir une interface partagée — un ensemble de méthodes que chaque sous-classe concrète doit implémenter. Python fournit cela via le module intégré abc (Abstract Base Classes).
Ce chapitre couvre :
- Pourquoi les classes abstraites existent et quand les utiliser
- Le module
abc—ABCetabstractmethod - Imposer des contrats de méthodes avec
@abstractmethod - Les propriétés abstraites
- Les méthodes concrètes dans une classe abstraite
- Les classes abstraites vs. lever
NotImplementedError - Un exemple concret
Avant de lire ce chapitre, assurez-vous d'être à l'aise avec les classes et objets Python et l'héritage.
Pourquoi utiliser des classes abstraites ?
Au fur et à mesure que vous ajoutez des sous-classes à une hiérarchie, il devient facile d'oublier d'implémenter une méthode requise. Le problème ne se manifeste qu'à l'exécution, au cœur de votre code, ce qui rend le débogage pénible.
Les classes de base abstraites résolvent ce problème en déplaçant l'erreur au moment où une classe est instanciée — et non au moment où une méthode manquante est appelée. Elles agissent comme un contrat : chaque sous-classe concrète doit implémenter chaque méthode abstraite, ou Python refusera de créer un objet à partir d'elle.
Classes abstraites vs. lever NotImplementedError
Avant l'existence du module abc, le schéma courant consistait à lever NotImplementedError dans une méthode de classe de base :
class Shape:
def area(self):
raise NotImplementedError("Subclasses must implement area()")
class Circle(Shape):
pass # forgot to implement area()
c = Circle() # No error yet — Python lets this through
c.area() # NotImplementedError only here, at call timeLa faiblesse : Python crée l'objet sans problème. L'erreur est invisible jusqu'à ce que la méthode soit réellement appelée, ce qui peut se produire dans une partie complètement différente du programme.
Avec une classe abstraite, l'erreur se produit immédiatement :
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
pass # still missing area()
c = Circle()
# TypeError: Can't instantiate abstract class Circle
# with abstract method areaLa classe est défectueuse au moment de la création — exactement là où le contrat a été violé.
Le module abc
Le module abc de Python fournit deux outils que vous utiliserez en permanence :
| Nom | Ce que c'est |
|---|---|
ABC | Une classe de base utilitaire. Héritez-en pour rendre votre classe abstraite. |
abstractmethod | Un décorateur qui marque une méthode comme abstraite. |
Importez-les ainsi :
from abc import ABC, abstractmethodCréer une classe abstraite
Héritez de ABC et décorez au moins une méthode avec @abstractmethod :
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
passShape déclare maintenant que tout ce qui se dit être une forme doit fournir area() et perimeter(). Vous ne pouvez pas créer de Shape directement :
s = Shape()
# TypeError: Can't instantiate abstract class Shape
# with abstract methods area, perimeterImplémenter une classe abstraite
Une classe concrète est celle qui hérite de la classe abstraite et implémente chaque méthode abstraite. Ce n'est qu'à ce moment-là que vous pouvez créer des instances :
Implémenter une classe abstraite avec Circle et Rectangle
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
c = Circle(5)
r = Rectangle(4, 6)
print(round(c.area(), 4)) # 78.5398
print(round(c.perimeter(), 4)) # 31.4159
print(r.area()) # 24
print(r.perimeter()) # 20Circle et Rectangle implémentent toutes les méthodes abstraites, Python leur permet donc d'être instanciées.
Que se passe-t-il si vous oubliez une méthode ?
Si une sous-classe n'implémente que certaines méthodes abstraites, elle reste abstraite — son instanciation lève un TypeError qui indique la méthode manquante :
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class IncompleteShape(Shape):
def area(self):
return 0
# perimeter() not implemented
s = IncompleteShape()
# TypeError: Can't instantiate abstract class IncompleteShape
# with abstract method perimeterLe message d'erreur vous indique exactement quelle méthode vous avez oubliée — bien plus utile qu'un crash générique à l'exécution.
Propriétés abstraites
Vous pouvez également rendre une propriété abstraite. Cela force les sous-classes à fournir une propriété (ou un attribut calculé à partir d'une propriété), et non une simple méthode. Utilisez @property et @abstractmethod ensemble, avec @property en premier :
Utiliser des propriétés abstraites
from abc import ABC, abstractmethod
class Animal(ABC):
@property
@abstractmethod
def sound(self):
pass
def describe(self):
return f"I make the sound: {self.sound}"
class Dog(Animal):
@property
def sound(self):
return "Woof"
class Cat(Animal):
@property
def sound(self):
return "Meow"
dog = Dog()
cat = Cat()
print(dog.describe()) # I make the sound: Woof
print(cat.describe()) # I make the sound: MeowLa propriété sound dans chaque sous-classe se comporte comme un attribut en lecture seule du point de vue de l'appelant, tandis que la classe abstraite garantit que chaque sous-classe concrète en fournit une.
Méthodes concrètes dans une classe abstraite
Les classes abstraites ne se limitent pas aux méthodes abstraites. Elles peuvent contenir des méthodes entièrement implémentées (concrètes) que toutes les sous-classes partagent automatiquement. C'est la différence essentielle entre les classes abstraites et les interfaces simples dans d'autres langages — vous pouvez mettre de la logique partagée dans la classe abstraite :
Partager une logique commune via une méthode concrète
from abc import ABC, abstractmethod
class Logger(ABC):
def log(self, message):
"""Concrete method — shared by all subclasses."""
formatted = self.format_message(message)
self.write(formatted)
def format_message(self, message):
return f"[LOG] {message}"
@abstractmethod
def write(self, message):
"""Abstract — each subclass decides how to output."""
pass
class ConsoleLogger(Logger):
def write(self, message):
print(message)
class FileLogger(Logger):
def __init__(self):
self.entries = []
def write(self, message):
self.entries.append(message)
console = ConsoleLogger()
console.log("Server started") # prints: [LOG] Server started
file_log = FileLogger()
file_log.log("Database connected")
print(file_log.entries) # ['[LOG] Database connected']Ici log() et format_message() sont des méthodes concrètes partagées. Seul write() — la partie qui diffère entre les backends — est abstraite. Les sous-classes n'ont jamais besoin de réimplémenter la logique de formatage.
isinstance() et les classes abstraites
Un objet qui est une instance d'une sous-classe concrète est également considéré comme une instance de la classe de base abstraite. Cela vous permet d'écrire des fonctions qui acceptent tout objet conforme à l'interface, sans se soucier du type spécifique :
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
shapes = [Circle(3), Rectangle(4, 5)]
for shape in shapes:
print(isinstance(shape, Shape)) # True for both
def total_area(shapes):
return sum(s.area() for s in shapes)
print(round(total_area(shapes), 4)) # 48.2743C'est l'avantage fondamental : total_area() fonctionne avec n'importe quelle Shape — passée, présente ou future — tant que l'objet implémente l'interface.
Exemple concret : processeurs de paiement
Les classes abstraites brillent lorsque vous avez besoin de plusieurs implémentations d'un même concept. Considérez un système de paiement qui doit prendre en charge plusieurs fournisseurs de paiement :
Classe abstraite pour le traitement des paiements
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def charge(self, amount):
pass
@abstractmethod
def refund(self, amount):
pass
def process(self, amount):
"""Concrete method — shared processing logic."""
print(f"Processing payment of ${amount}")
self.charge(amount)
class StripeProcessor(PaymentProcessor):
def charge(self, amount):
print(f"Stripe: charged ${amount}")
def refund(self, amount):
print(f"Stripe: refunded ${amount}")
class PayPalProcessor(PaymentProcessor):
def charge(self, amount):
print(f"PayPal: charged ${amount}")
def refund(self, amount):
print(f"PayPal: refunded ${amount}")
processors = [StripeProcessor(), PayPalProcessor()]
for p in processors:
p.process(100)
# Processing payment of $100
# Stripe: charged $100
# Processing payment of $100
# PayPal: charged $100Ajouter un troisième fournisseur de paiement — par exemple BraintreeProcessor — nécessite uniquement d'implémenter charge() et refund(). La logique de process() et les vérifications isinstance continuent de fonctionner sans modification.
Quand utiliser des classes abstraites
Utilisez une classe abstraite lorsque :
- Vous avez une famille de classes apparentées qui doivent toutes fournir certaines méthodes.
- Vous souhaitez partager du code (méthodes concrètes) tout en imposant que d'autres méthodes soient redéfinies.
- Vous voulez que Python lève une erreur immédiatement si une sous-classe est incomplète, plutôt que d'attendre un crash à l'exécution.
Vous n'avez probablement pas besoin d'une classe abstraite lorsque :
- Vous n'avez qu'une ou deux sous-classes et la hiérarchie est peu susceptible de croître.
- L'interface partagée est déjà imposée par d'autres moyens (par ex., les dataclasses Python ou le duck typing).
- Vous souhaitez simplement empêcher l'instanciation directe — une solution plus simple est un hook
__init_subclass__ou un simple commentaire.
Pour les concepts étroitement liés, consultez l'héritage Python et le polymorphisme Python.