30. ABC — Abstract Base Classes

abc.ABC, @abstractmethod, ABCMeta, @abstractproperty, register(), __subclasshook__, ABC vs Protocol.


30.1 Principe

Une classe abstraite (ABC) définit une interface que les sous-classes doivent implémenter. Elle ne peut pas être instanciée directement.

from abc import ABC, abstractmethod
 
class Forme(ABC):
    @abstractmethod
    def aire(self) -> float:
        pass
 
    @abstractmethod
    def périmètre(self) -> float:
        pass
 
# f = Forme()  # TypeError: Can't instantiate abstract class

30.2 Implémentation concrète

class Cercle(Forme):
    def __init__(self, rayon: float):
        self.rayon = rayon
 
    def aire(self) -> float:
        return 3.14159 * self.rayon ** 2
 
    def périmètre(self) -> float:
        return 2 * 3.14159 * self.rayon
 
class Rectangle(Forme):
    def __init__(self, largeur: float, hauteur: float):
        self.largeur = largeur
        self.hauteur = hauteur
 
    def aire(self) -> float:
        return self.largeur * self.hauteur
 
    def périmètre(self) -> float:
        return 2 * (self.largeur + self.hauteur)
 
c = Cercle(5)
print(c.aire())  # 78.53975

Classe concrète = implémente toutes les méthodes abstraites.

30.3 @abstractmethod — décorateur

Marque une méthode comme abstraite. Toute sous-classe doit la redéfinir.

class Base(ABC):
    @abstractmethod
    def obligatoire(self):
        ...
 
    @abstractmethod
    def aussi_obligatoire(self):
        ...
 
    def optionnelle(self):
        """Méthode concrète — pas d'obligation."""
        return "défaut"

Une classe avec au moins une méthode abstraite est abstraite : impossible d’instancier.

# class Impl(Base):
#     pass  # TypeError si on essaie d'instancier

30.4 @abstractmethod + autres décorateurs

Combinaison possible avec @property, @classmethod, @staticmethod :

from abc import ABC, abstractmethod
 
class Base(ABC):
    @property
    @abstractmethod
    def nom(self) -> str:
        ...
 
    @classmethod
    @abstractmethod
    def from_config(cls, config: dict) -> "Base":
        ...
 
    @staticmethod
    @abstractmethod
    def helper(x: int) -> int:
        ...

Implémentation :

class Concrète(Base):
    @property
    def nom(self) -> str:
        return "Concrète"
 
    @classmethod
    def from_config(cls, config: dict) -> "Concrète":
        return cls()
 
    @staticmethod
    def helper(x: int) -> int:
        return x ** 2

Ordre : @abstractmethod doit être le décorateur le plus interne.

# Correct
@property
@abstractmethod
def nom(self): ...
 
# Incorrect
@abstractmethod
@property
def nom(self): ...  # TypeError

30.5 ABCs de la bibliothèque standard

Python fournit de nombreuses ABCs dans collections.abc :

from collections.abc import (
    Container, Hashable, Iterable, Iterator,
    Sequence, MutableSequence,
    Set, MutableSet,
    Mapping, MutableMapping,
    Callable,
    Sized,
)
 
# Vérification
isinstance([1, 2, 3], Sequence)    # True
isinstance({1, 2, 3}, Set)         # True
isinstance({"a": 1}, Mapping)      # True
isinstance(print, Callable)        # True
isinstance(42, Hashable)           # True
# Héritage d'une ABC standard
class MaListe(Sequence):
    def __init__(self, items):
        self._items = list(items)
 
    def __getitem__(self, index):
        return self._items[index]
 
    def __len__(self):
        return len(self._items)
 
    # Sequence nous donne __contains__, __iter__, __reversed__,
    # index(), count() gratuitement grâce à __getitem__ + __len__
ABCMéthodes abstraitesMéthodes fournies
Container__contains__
Hashable__hash__
Iterable__iter__
Iterator__next__, __iter__
Sequence__getitem__, __len____contains__, __iter__, __reversed__, index, count
MutableSequence+ __setitem__, __delitem__, insertappend, extend, pop, remove, __iadd__
Set__contains__, __iter__, __len____le__, __lt__, __gt__, __ge__, __eq__, __ne__, __and__, __or__, __sub__, __xor__, isdisjoint
Mapping__getitem__, __iter__, __len____contains__, keys, items, values, get, __eq__
Callable__call__
Sized__len__

30.6 register() — sous-classes virtuelles

Permet de déclarer qu’une classe est une sous-classe sans héritage :

from abc import ABC, abstractmethod
 
class Volant(ABC):
    @abstractmethod
    def voler(self) -> None:
        ...
 
class Oiseau:  # n'hérite PAS de Volant
    def voler(self) -> None:
        print("L'oiseau vole")
 
# Enregistrement comme sous-classe virtuelle
Volant.register(Oiseau)
 
isinstance(Oiseau(), Volant)  # True
issubclass(Oiseau, Volant)    # True

Cas concret : list est une sous-classe virtuelle de Sequence (héritage réel de object, mais Sequence.register(list) a été appelé).

from collections.abc import Sequence
issubclass(list, Sequence)   # True (sous-classe virtuelle)
isinstance([1, 2], Sequence) # True

Notre propre ABC avec enregistrement

from abc import ABC
 
class Plugin(ABC):
    @abstractmethod
    def exécuter(self) -> None:
        ...
 
# Découverte automatique des plugins
class MonPlugin:
    def exécuter(self) -> None:
        print("Exécution")
 
Plugin.register(MonPlugin)
issubclass(MonPlugin, Plugin)  # True

30.7 __subclasshook__ — détection automatique

Permet à une ABC de reconnaître des sous-classes sans register() :

from abc import ABC, abstractmethod
 
class Volant(ABC):
    @abstractmethod
    def voler(self) -> None:
        ...
 
    @classmethod
    def __subclasshook__(cls, other):
        if cls is Volant:
            # Vérifie si la classe a une méthode voler
            for c in other.__mro__:
                if "voler" in c.__dict__:
                    return True
        return NotImplemented
 
class Avion:  # pas d'héritage, pas de register()
    def voler(self) -> None:
        print("L'avion vole")
 
issubclass(Avion, Volant)  # True (via __subclasshook__)

Avec @runtime_checkable (voir leçon 17) :

from typing import Protocol, runtime_checkable
 
@runtime_checkable
class Volant(Protocol):
    def voler(self) -> None:
        ...
 
isinstance(Avion(), Volant)  # True (même mécanisme)

30.8 ABC avec méthodes concrètes

Une ABC peut avoir des méthodes concrètes (implémentées) :

from abc import ABC, abstractmethod
 
class Apprentissage(ABC):
    @abstractmethod
    def fit(self, X, y) -> None:
        ...
 
    @abstractmethod
    def predict(self, X):
        ...
 
    def fit_predict(self, X, y):
        """Méthode concrète — déjà implémentée."""
        self.fit(X, y)
        return self.predict(X)
 
    @property
    def nom(self) -> str:
        return self.__class__.__name__

30.9 ABC avec __init__

Une ABC peut avoir un __init__ — appelé par super().__init__() dans les sous-classes :

from abc import ABC, abstractmethod
 
class Modèle(ABC):
    def __init__(self, nom: str, version: int = 1):
        self.nom = nom
        self.version = version
        self._entraîné = False
 
    @abstractmethod
    def fit(self, X, y) -> None:
        ...
 
    @abstractmethod
    def predict(self, X):
        ...
 
class Réseau(Modèle):
    def __init__(self, hidden_size: int = 256):
        super().__init__("Réseau", version=2)
        self.hidden_size = hidden_size
 
    def fit(self, X, y) -> None:
        self._entraîné = True
 
    def predict(self, X):
        return [0] * len(X)

30.10 ABCs imbriquées

from abc import ABC, abstractmethod
 
class Exporteur(ABC):
    @abstractmethod
    def exporter(self, données) -> str:
        ...
 
class ExporteurJSON(Exporteur):
    def exporter(self, données) -> str:
        import json
        return json.dumps(données)
 
class ExporteurCSV(Exporteur):
    def exporter(self, données) -> str:
        ...
 
class Rapport(ABC):
    @abstractmethod
    def générer(self) -> str:
        ...
 
    @abstractmethod
    def exporteur(self) -> Exporteur:
        ...  # retourne une ABC
 
class RapportPDF(Rapport):
    def générer(self) -> str:
        return "contenu"
 
    def exporteur(self) -> Exporteur:
        return ExporteurJSON()  # OK : retourne une sous-classe

30.11 ABC vs Protocol

ABCProtocol
Héritage explicite requis (sauf register)Duck typing implicite
Vérification runtime (isinstance)@runtime_checkable optionnel
Peut avoir des méthodes concrètesMéthodes abstraites uniquement
@abstractmethod obligatoireDéfaut : aucune méthode obligatoire
Plus lourd, héritage formelPlus léger, typage statique
Traditionnel, connuModerne (PEP 544, 3.8+)
from abc import ABC, abstractmethod
 
# Approche ABC (héritage formel)
class Forme(ABC):
    @abstractmethod
    def aire(self) -> float: ...
 
class Cercle(Forme):
    def aire(self) -> float: return 3.14 * self.r ** 2
 
# Approche Protocol (duck typing statique)
from typing import Protocol
 
class Forme(Protocol):
    def aire(self) -> float: ...
 
class Cercle:
    def aire(self) -> float: return 3.14 * self.r ** 2
    # pas d'héritage explicite, mais satisfait le Protocol

Quand utiliser ABC ?

  • Tu veux forcer un héritage explicite (« ma classe est un X »)
  • Tu veux fournir des méthodes concrètes (template method pattern)
  • Tu veux isinstance / issubclass sans décorateur
  • Tu travailles avec du code legacy ou des frameworks qui attendent des ABCs

Quand utiliser Protocol ?

  • Tu veux du duck typing statique (si ça marche, c’est bon)
  • Tu veux éviter un couplage d’héritage fort
  • Tu utilises mypy ou pyright pour la vérification statique
  • Tu préfères la composition à l’héritage

30.12 ABC vs @dataclass

ABC et dataclass répondent à des besoins orthogonaux — ce n’est pas un choix exclusif.

CritèreABC@dataclass
RôleDéfinir une interface / contratStocker des données avec auto-génération
Ce qu’elle faitForce l’implémentation de méthodesGénère __init__, __repr__, __eq__, __hash__
Méthodes abstraitesOui — c’est le butNon
AttributsNon déclarés (sauf via @property)Déclarés comme champs typés
HéritageCentral (cascade d’interfaces)Possible mais secondaire
InstanciationImpossible si méthodes abstraites manquentPossible si tous les champs sont fournis
TypageComportementÉtat

Quand les utiliser ensemble ?

C’est même recommandé — une ABC pour l’interface, une dataclass pour l’implémentation :

from abc import ABC, abstractmethod
from dataclasses import dataclass
 
# 1. ABC définit le contrat (comportement)
class Modèle(ABC):
    @abstractmethod
    def fit(self, X, y) -> None:
        ...
 
    @abstractmethod
    def predict(self, X):
        ...
 
# 2. Dataclass implémente le contrat + stocke l'état
@dataclass
class RégressionLogistique(Modèle):
    learning_rate: float = 0.01
    epochs: int = 100
    _poids: list[float] | None = None
 
    def fit(self, X, y) -> None:
        # implémentation concrète
        ...
 
    def predict(self, X):
        ...

Quand choisir l’un sans l’autre ?

# ABC seule : tu veux une interface pure, pas d'état
class Plugin(ABC):
    @abstractmethod
    def exécuter(self) -> None: ...
 
    @abstractmethod
    def annuler(self) -> None: ...
 
# Dataclass seule : tu veux un conteneur de données, pas de comportement contraint
@dataclass
class Config:
    learning_rate: float = 0.001
    batch_size: int = 64
    epochs: int = 100

Piège : ne pas confondre héritage et composition

# ❌ Mauvaise idée : ABC pour des données
class Animal(ABC):
    @abstractmethod
    def nom(self) -> str: ...  # un getter abstrait pour un simple champ ?
 
# ✅ Préférer une dataclass
@dataclass
class Animal:
    nom: str
    âge: int

Règle empirique :

  • ABC si tu veux garantir qu’une méthode existe (contrat comportemental)
  • @dataclass si tu veux éviter d’écrire __init__ / __repr__ / __eq__ (conteneur de données)
  • Les deux si tu veux une interface formelle + une implémentation avec état

30.13 Erreurs communes

# Erreur : oublier @abstractmethod
class Base(ABC):
    def obligatoire(self): ...  # pas abstraite ! sous-classe non forcée
 
# Erreur : instancier une classe abstraite
class Base(ABC):
    @abstractmethod
    def f(self): ...
 
# b = Base()  # TypeError
 
# Erreur : ne pas implémenter toutes les méthodes
class Impl(Base):
    def f(self): ...  # OK
 
class Partielle(Base):
    pass  # toujours abstraite
 
# p = Partielle()  # TypeError

🔗 ← Retour au cours · ← précédent · Index