18. Métaclasses
type(),metaclass=,__new__de métaclasse, cas concrets : singleton, registre, validation de classe, DSL.
18.1 Principe
Une métaclasse est la classe d’une classe. type est la métaclasse par défaut.
class MaClasse:
pass
# Équivaut à
MaClasse = type("MaClasse", (), {})Hiérarchie :
type → métaclasse (la classe des classes)
↓
MaClasse → classe
↓
obj → instance
18.2 type(name, bases, dict)
# Création dynamique de classe
MaClasse = type("MaClasse", (object,), {"x": 10, "f": lambda self: self.x})
obj = MaClasse()
obj.f() # 1018.3 Métaclasse personnalisée
Une métaclasse hérite de type et surcharge __new__ :
class MaMéta(type):
def __new__(mcs, name, bases, namespace, **kwargs):
print(f"Création de {name}")
# namespace = dict des attributs et méthodes
namespace["créé_par"] = mcs.__name__
return super().__new__(mcs, name, bases, namespace)
class MaClasse(metaclass=MaMéta):
pass
# Création de MaClasse
MaClasse.créé_par # "MaMéta"__new__ vs __init__ dans une métaclasse
class MaMéta(type):
def __new__(mcs, name, bases, namespace):
print(f"__new__: {name}")
cls = super().__new__(mcs, name, bases, namespace)
return cls
def __init__(cls, name, bases, namespace, **kwargs):
print(f"__init__: {name}")
super().__init__(name, bases, namespace)__new__: avant la création de la classe (modifier le namespace)__init__: après la création (initialiser la classe)
__call__ : contrôle des instances
class SingletonMeta(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 Configuration(metaclass=SingletonMeta):
def __init__(self):
self.paramètres = {}
c1 = Configuration()
c2 = Configuration()
c1 is c2 # True18.4 Cas concrets
1. Validation de classe
Garantir qu’une classe implémente certaines méthodes :
class InterfaceMeta(type):
def __new__(mcs, name, bases, namespace):
if name != "PluginBase":
obligatoires = ["exécuter", "nom"]
for attr in obligatoires:
if attr not in namespace:
raise TypeError(
f"{name} doit implémenter {attr}"
)
return super().__new__(mcs, name, bases, namespace)
class PluginBase(metaclass=InterfaceMeta):
pass
class MonPlugin(PluginBase):
nom = "mon-plugin"
def exécuter(self):
print("Exécution")
# class MauvaisPlugin(PluginBase): # TypeError !
# pass2. Registre automatique
class RegistreMeta(type):
registry = {}
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if name != "OperationBase":
RegistreMeta.registry[name] = cls
return cls
class OperationBase(metaclass=RegistreMeta):
def apply(self, x): ...
class Addition(OperationBase):
def apply(self, x, y): return x + y
class Multiplication(OperationBase):
def apply(self, x, y): return x * y
print(RegistreMeta.registry)
# {'Addition': <class Addition>, 'Multiplication': <class Multiplication>}3. Ajout d’attributs automatique
class AutoProps(type):
def __new__(mcs, name, bases, namespace):
annotations = namespace.get("__annotations__", {})
for attr_name, attr_type in annotations.items():
if attr_name.startswith("_") or attr_name in namespace:
continue
# Crée des accesseurs automatiques
namespace[attr_name] = property(
lambda self, n=attr_name: getattr(self, f"_{n}"),
lambda self, val, n=attr_name: setattr(self, f"_{n}", val),
)
return super().__new__(mcs, name, bases, namespace)
class Point(metaclass=AutoProps):
x: float
y: float
def __init__(self, x, y):
self._x = x
self._y = y
p = Point(1, 2)
print(p.x) # 1
p.y = 54. DSL (Domain Specific Language)
class DSLMeta(type):
def __new__(mcs, name, bases, namespace):
# Convertit les méthodes en commandes enregistrées
cls = super().__new__(mcs, name, bases, namespace)
cls._commandes = {}
for attr_name, attr_val in namespace.items():
if callable(attr_val) and not attr_name.startswith("_"):
cls._commandes[attr_name] = attr_val
return cls
def __call__(cls, *args, **kwargs):
instance = super().__call__(*args, **kwargs)
return instance
class Script(metaclass=DSLMeta):
pass
class MonScript(Script):
def saluer(self, nom):
print(f"Bonjour {nom}")
def calculer(self, x, y):
return x + y
# MonScript._commandes = {"saluer": ..., "calculer": ...}18.5 Métaclasse + __init_subclass__
Alternative plus simple aux métaclasses (PEP 487, 3.6+) :
class PluginBase:
registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
PluginBase.registry[cls.__name__] = cls
class MonPlugin(PluginBase):
passQuand utiliser __init_subclass__ vs métaclasse ?
__init_subclass__ | Métaclasse |
|---|---|
| Simple hook à la création | Contrôle total de la création |
| Pas de conflit si plusieurs héritages | Risque de conflit entre métaclasses |
| Suffisant pour registre/validation | Nécessaire pour DSL, __call__, __new__ avancé |
18.6 __prepare__ — contrôler le namespace
class OrdonnéMeta(type):
@classmethod
def __prepare__(mcs, name, bases, **kwargs):
return {} # dict normal, ou OrderedDict pour préserver l'ordre
class Exemple(metaclass=OrdonnéMeta):
a = 1
b = 218.7 Métaclasse pour __slots__ automatique
class SlotsMeta(type):
def __new__(mcs, name, bases, namespace):
annotations = namespace.get("__annotations__", {})
if "__slots__" not in namespace and not any(
hasattr(b, "__slots__") for b in bases
):
namespace["__slots__"] = tuple(annotations.keys())
return super().__new__(mcs, name, bases, namespace)
class Point(metaclass=SlotsMeta):
x: float
y: float
def __init__(self, x, y):
self.x = x
self.y = y
# p = Point(1, 2)
# p.z = 3 # AttributeError18.8 Avertissements
- Les métaclasses créent une complexité cognitive élevée
- Une métaclasse peut entrer en conflit avec une autre (solution : préférer
__init_subclass__) - 90% des cas d’usage sont couverts par
__init_subclass__, les décorateurs de classe, ou__set_name__ - N’utiliser une métaclasse que quand les alternatives échouent