W3docs

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 abcABC et abstractmethod
  • 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 time

La 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 area

La 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 :

NomCe que c'est
ABCUne classe de base utilitaire. Héritez-en pour rendre votre classe abstraite.
abstractmethodUn décorateur qui marque une méthode comme abstraite.

Importez-les ainsi :

from abc import ABC, abstractmethod

Cré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):
        pass

Shape 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, perimeter

Implé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())            # 20

Circle 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 perimeter

Le 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: Meow

La 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.2743

C'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 $100

Ajouter 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.

Pratique

Pratique
What happens when you try to instantiate a class that inherits from ABC but does not implement all abstract methods?
What happens when you try to instantiate a class that inherits from ABC but does not implement all abstract methods?
Was this page helpful?