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 class30.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.53975Classe 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'instancier30.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 ** 2Ordre : @abstractmethod doit être le décorateur le plus interne.
# Correct
@property
@abstractmethod
def nom(self): ...
# Incorrect
@abstractmethod
@property
def nom(self): ... # TypeError30.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__| ABC | Méthodes abstraites | Méthodes fournies |
|---|---|---|
Container | __contains__ | — |
Hashable | __hash__ | — |
Iterable | __iter__ | — |
Iterator | __next__, __iter__ | — |
Sequence | __getitem__, __len__ | __contains__, __iter__, __reversed__, index, count |
MutableSequence | + __setitem__, __delitem__, insert | append, 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) # TrueCas 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) # TrueNotre 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) # True30.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-classe30.11 ABC vs Protocol
| ABC | Protocol |
|---|---|
Héritage explicite requis (sauf register) | Duck typing implicite |
Vérification runtime (isinstance) | @runtime_checkable optionnel |
| Peut avoir des méthodes concrètes | Méthodes abstraites uniquement |
@abstractmethod obligatoire | Défaut : aucune méthode obligatoire |
| Plus lourd, héritage formel | Plus léger, typage statique |
| Traditionnel, connu | Moderne (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 ProtocolQuand 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/issubclasssans 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ère | ABC | @dataclass |
|---|---|---|
| Rôle | Définir une interface / contrat | Stocker des données avec auto-génération |
| Ce qu’elle fait | Force l’implémentation de méthodes | Génère __init__, __repr__, __eq__, __hash__ |
| Méthodes abstraites | Oui — c’est le but | Non |
| Attributs | Non déclarés (sauf via @property) | Déclarés comme champs typés |
| Héritage | Central (cascade d’interfaces) | Possible mais secondaire |
| Instanciation | Impossible si méthodes abstraites manquent | Possible si tous les champs sont fournis |
| Typage | Comportement | É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 = 100Piè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: intRègle empirique :
- ABC si tu veux garantir qu’une méthode existe (contrat comportemental)
@dataclasssi 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