En Python, tout est un objet, y compris les classes elles-mêmes. Les métaclasses sont les classes qui définissent comment les autres classes sont créées. L'objet intégré type est la métaclass par défaut en Python, permettant une personnalisation avancée du mécanisme de définition des classes.
Pour vérifier le type d'un objet, on utilise type() :
print(type(42))
print(type("hello"))
print(type([1, 2, 3]))
Dans un exemple simple, définisons une classe et observons son type :
class Personne:
def __init__(self, nom):
self.nom = nom
p1 = Personne(nom="Alice")
print(type(p1)) # Affiche <class '__main__.Personne'>
print(type(Personne)) # Affiche <class 'type'>
Cela montre que les classes standard, comme dict ou list, sont des instances de type, qui agit comme une métaclass.
Création de classes avec type
Les classes peuvent être définies de deux manières principales : via le mot-clé class ou en appelant directement type.
Utilisation du mot-clé class
class Animal:
def __init__(self, espece):
self.espece = espece
print(type(Animal)) # <class 'type'>
Utilisation de type
La fonction type peut créer dynamiquement une classe. Sa signature est type(nom, bases, espace).
def crier(self):
print(f"{self.espece} crie!")
Chien = type("Chien", (Animal,), {"crier": crier})
print(type(Chien)) # <class 'type'>
print(Chien.__dict__)
# Équivalent avec la syntaxe class :
class Chat(Animal):
crier = crier
print(type(Chat))
print(Chat.__dict__)
Les deux approches produisent des dictionnaires d'attributs similaires pour les classes créées.
Rôle des métaclasses
Les classes contrôlent la création d'objets, tandis que les métaclasses contrôlent la création de classes, permettant de personnaliser leurs propriétés.
Implémentation d'une métaclass personnalisée
Imposons une contrainte : tous les noms de classes doivent être en majuscules.
Création de la métaclass
class MetaClassePersonnalisee(type):
def __init__(cls, nom, bases, espace):
if not nom.isupper():
raise TypeError(f"Le nom de la classe {nom} doit être en majuscules.")
super().__init__(nom, bases, espace)
# Utilisation :
class UTILISATEUR(metaclass=MetaClassePersonnalisee):
pass
# Cela lèverait une erreur :
# class utilisateur(metaclass=MetaClassePersonnalisee): ...
Personnalisation de la création d'objets
Pour contrôler l'instanciation, on redéfinit __call__ dans la métaclass.
class MetaClasseStricte(type):
def __call__(cls, *args, **kwargs):
if args:
raise TypeError("Les paramètres doivent être nommés.")
instance = super().__call__(*args, **kwargs)
return instance
class Configuration(metaclass=MetaClasseStricte):
def __init__(self, parametre):
self.parametre = parametre
# Correct :
config1 = Configuration(parametre="valeur")
# Incorrect :
# config2 = Configuration("valeur") # TypeError
Méthodes __new__ et __call__
La méthode __new__ crée le squelette de l'objet, tandis que __init__ initialise ses attributs. Dans une métaclass, __new__ et __init__ gèrent la classe elle-même.
class MetaClasseAvancee(type):
def __new__(cls, nom, bases, espace):
print("Création de la classe avec __new__")
return super().__new__(cls, nom, bases, espace)
def __init__(cls, nom, bases, espace):
print("Initialisation de la classe avec __init__")
super().__init__(nom, bases, espace)
def __call__(cls, *args, **kwargs):
print("Appel de la classe via __call__")
return super().__call__(*args, **kwargs)
class Produit(metaclass=MetaClasseAvancee):
def __init__(self, nom):
self.nom = nom
p = Produit("Livre") # Déclenche les méthodes de la métaclass
Lors de l'instanciation, la séquence est : métaclass __call__ → classe __new__ → classe __init__. Évitez d'appeler explicitement __init__ dans __new__ pour prévenir les doubles appels.