Métaclasses¶
Principe¶
[sec:metaclasses] Les métaclasses sont les classes qui instancient
d’autres classes. Par défaut, une seule métaclasse est définie : la
métaclasse type
. On s’en rend compte en demandant le type des
classes que l’on crée.
>>> class MaClasse:
... pass
...
>>> type(MaClasse)
<class 'type'>
La métaclasse type
¶
On sait que passer à type()
un objet renvoie son type, c’est-à-dire
l’objet qui l’a instancié. Mais type()
permet aussi des créer des
types (des classes) à la volée si on lui passe plus d’arguments : un
nom, un itérable contenant ses classes parentes et un dictionnaire
contenant ses attributs.
>>> MyType = type("MyType", (), {}) # une classe on ne peut plus basique
>>> MyType()
<__main__.MyType object at 0x7ff838fb7518>
>>> issubclass(MyType, object)
True
>>> isisntance(MyType, type)
True
On peut donc créer des fonctions qui créent des classes.
def class_creator(name, bases=(), attrs={}): # il faut garder la signature de type()
"""Créateur de classe personnalisé.
Celui ajoute à chaque classe créée un identifiant de classe correspondant au
nombre de classes créées au moment de l'appel de class_creator().
"""
if not hasattr(class_creator, "increment"):
class_creator.increment = 0 # on utilise un attribut de fonction
attrs["class_id"] = class_creator.increment
class_creator.increment += 1
return type(name, bases, attrs)
>>> first = class_creator("first")
>>> second = class_creator("second")
>>> first.class_id
0
>>> second.class_id
1
>>> class Third(metaclass=class_creator):
... pass
...
>>> Third.class_id
2
Ou plutôt des générateurs de classes :
def class_generator_function(bases=(), attrs={}):
"""Générateur de classe."""
increment = 0
name = yield # initialisation
while True:
attrs["class_id"] = increment
name = yield type(name, bases, attrs)
increment += 1
>>> class_generator = class_generator_function()
>>> next(class_generator) # initialisation
>>> first = class_generator.send("first")
>>> second = class_generator.send("second")
>>> first.class_id
0
>>> second.class_id
1
Ecrire une métaclasse¶
On est un peu limité dans le cas de fonctions qui créent des classes. Pour des choses plus complexes, on peut écrire des classes qui instancient d’autres classes : des métaclasses.
Pour qu’une classe puisse instancier d’autres classes, il faut hériter
de type
. Cela permet notamment d’hériter de sa fonction
__new__()
. D’habitude (c’est-à-dire quand on hérite simplement de
object
), cette fonction ne fait rien de spécial, elle retourne
simplement un objet vide que l’on initialise dans __init__()
. Dans
le cas de type
, c’est cette fonction qui est chargée de créer la
classe. Pour indiquer que l’on est en train de définir une métaclasse,
on écrit cls
au lieu de self
pour faire référence à l’objet
instancié, et mcls
au lieu de cls
pour faire référence au type.
Cette métaclasse ne fait rien de plus que type
:
class SimpleMeta(type):
def __new__(mcls, name, bases, attrs):
print(name, "was created.")
return super().__new__(mcls, name, bases, attrs)
class ClassUsingSimpleMeta(metaclass=SimpleMeta):
pass
A l’exécution on verra : |ClassUsingSimpleMeta was created.|
Si l’on veut le même comportement que les exemples précédents :
class SimpleMeta(type):
class_id = 0
def __init__(cls, name, bases, attrs): # on récupère les arguments de __new__
super().__init__(name, bases, attrs)
cls.class_id = cls.class_id
type(cls).class_id += 1
>>> class Class0(metaclass=SimpleMeta):
... pass
...
>>> Class2 = SimpleMeta("Class2", (), {})
>>> Class0.class_id
0
>>> Class1.class_id
1
On peut ajouter des paramètres lors de la déclaration d’une classe :
class AbstractoOrNotAbstractMeta(type):
def __new__(mcls, name, bases, attrs, abstract=False):
cls = super().__new__(mcls, name, bases, attrs)
print(name)
if abstract:
def new(cls, *args, **kwargs):
raise TypeError("This class is abstract.")
cls.__new__ = new
else:
cls.__new__ = object.__new__
return cls
class Abstract(metaclass=AbstractoOrNotAbstractMeta, abstract=True):
pass
class Concrete(Abstract):
pass
Concrete()
Abstract() # TypeError
Application des métaclasses : propriété de classe¶
On pourrait imaginer des propriétés de classes afin d’ajouter une couche de logique sur une simple variable de classe. Au lieu de définir un descripteur générique, on créer une métaclasse qui aura comme propriété la future propriété de classe.
Exemple : Un exemple simple
class ClassPropertyMeta(type):
def __new__(mcs, name, bases, attrs):
"""Créateur personnalisé.
On redéfinit __new__ pour s'assurer que les éventuels setters des
propriétés soient appelés.
"""
cls = super().__new__(mcs, name, bases, {})
for attr, value in attrs.items():
setattr(cls, attr, val)
return cls
@property
def some_positive_attr(self):
return self._propriete
@some_positive_attr.setter
def some_positive_attr(self, value):
if value < 0:
raise ValueError("some_positive_integer must be > 0.")
self._propriete = value
class ClassPropertyOwner(metaclass=ClassPropertyMeta):
some_positive_attr = -1
Cette définition de classe va lever une exception ValueError
. Oui,
une déclaration de classe peut lever une exception.