Métaprogrammation
L'une des techniques les plus importantes du développement logiciel est le principe DRY (Don't Repeat Yourself) : éviter la duplication de code. Chaque fois que vous rencontrez du code hautement répétitif, il est avantageux de chercher une solution plus élégante.
| Outil | Niveau d'action | Usage typique |
|---|---|---|
| Décorateurs | Fonctions / méthodes | Ajouter un comportement sans modifier la fonction |
| Décorateurs de classe | Classes | Modifier une classe après sa définition |
| Métaclasses | Création des classes elles-mêmes | Contrôler la génération de classes entières |
Métaclasses
Tout est objet en Python
En Python, tout est associé à un type. Les types eux-mêmes sont des classes. On peut obtenir le type de n'importe quelle valeur avec la fonction type().
Exemple — Types des objets courants
num = 23
liste = [1, 2, 4]
nom = "ESSADDOUKI"
print("Type de num :", type(num)) #
print("Type de liste :", type(liste)) #
print("Type de nom :", type(nom)) # Type de num : <class 'int'> Type de liste : <class 'list'> Type de nom : <class 'str'>
Contrairement à C ou Java où int, float, char sont des types primitifs, en Python ce sont des instances de classes. On peut donc créer un nouveau type simplement en définissant une classe.
Exemple — Une classe définit un nouveau type
class Personne:
pass
p = Personne()
print("Type de p :", type(p)) #
print("Type de Personne:", type(Personne)) # Type de p : <class '__main__.Personne'> Type de Personne : <class 'type'>
type, qui est responsable de la création de toutes les classes.Métaclasse (type) → crée → Classe (Personne) → crée → Objet (p)
Exemple — Modifier une classe dynamiquement
Les classes étant des objets, on peut leur ajouter des attributs et méthodes après leur définition.
class Personne:
pass
# Ajout dynamique d'attribut et de méthode
Personne.nom = "ESSADDOUKI"
Personne.afficher = lambda self: print(f"Nom : {self.nom}")
p = Personne()
p.afficher() # Nom : ESSADDOUKINom : ESSADDOUKI
Créer une classe avec type()
La fonction type() peut être appelée de deux façons différentes :
| Appel | Arguments | Résultat |
|---|---|---|
type(objet) | 1 argument | Retourne le type de l'objet |
type(nom, bases, dict) | 3 arguments | Crée et retourne une nouvelle classe |
NouvelleClasse = type(
'NomClasse', # nom de la classe
(ClasseBase,), # tuple des classes parentes
{ # dictionnaire attributs/méthodes
'attribut': valeur,
'methode': fonction
}
)Exemple — Classe créée dynamiquement avec type()
def afficher(self):
print(f"Je suis : {self.__class__.__name__} | Nom : {self.nom}")
class Personne:
def presentation(self):
print("Je suis une personne")
# Création dynamique d'une classe héritant de Personne
Meta = type('Meta', (Personne,), {
'nom' : 'ESSADDOUKI',
'prenom' : 'Mostafa',
'afficher': afficher
})
print("Type de Meta :", type(Meta)) #
p = Meta()
print("Type de p :", type(p)) #
p.afficher()
p.presentation()
print("Nom :", p.nom)Type de Meta : <class 'type'> Type de p : <class '__main__.Meta'> Je suis : Meta | Nom : ESSADDOUKI Je suis une personne Nom : ESSADDOUKI
Créer une métaclasse personnalisée
Pour créer une métaclasse personnalisée, on hérite de type et on redéfinit __new__ et/ou __init__.
| Méthode | Rôle | Paramètres |
|---|---|---|
__new__(cls, clsname, bases, clsdict) | Crée et retourne la classe — appelée avant __init__ | Type, nom, bases, dictionnaire |
__init__(cls, clsname, bases, clsdict) | Initialise la classe après sa création | Idem |
class MaMeta(type):
def __new__(cls, clsname, bases, clsdict):
# modifications avant création
return super().__new__(cls, clsname, bases, clsdict)
class MaClasse(metaclass=MaMeta):
...Cas 1 — Interdire l'héritage multiple
Exemple — Métaclasse HeritageMultiple
class HeritageMultiple(type):
def __new__(cls, clsname, bases, clsdict):
# Interdire l'héritage de plusieurs classes à la fois
if len(bases) > 1:
raise TypeError("Héritage multiple non autorisé !")
return super().__new__(cls, clsname, bases, clsdict)
class Base(metaclass=HeritageMultiple):
pass
class A(Base): pass
class B(Base): pass
# ❌ Déclenche une TypeError
class C(A, B):
passTraceback (most recent call last):
File "meta.py", line 14, in __new__
raise TypeError("Héritage multiple non autorisé !")
TypeError: Héritage multiple non autorisé !Cas 2 — Contrôler la création d'instances
En redéfinissant __call__ dans la métaclasse, on peut contrôler ou interdire l'instanciation d'une classe.
Exemple — Métaclasse Restriction
class Restriction(type):
def __call__(cls, *args, **kwargs):
raise TypeError(f"Impossible d'instancier directement '{cls.__name__}'")
class Singleton(metaclass=Restriction):
pass
# ❌ Déclenche une TypeError
obj = Singleton()TypeError: Impossible d'instancier directement 'Singleton'
__call__, on peut implémenter le patron Singleton (une seule instance autorisée) via une métaclasse, une approche plus élégante que les solutions sans métaclasse.Cas 3 — Capturer l'ordre de définition des attributs
Une métaclasse peut enregistrer automatiquement l'ordre de déclaration des attributs d'une classe — utile pour la sérialisation, le mapping vers une base de données, etc.
Exemple — Validation de types avec OrderedMeta
class Typed:
"""Descripteur de base avec vérification de type."""
_expected_type = type(None)
def __init__(self, name=None):
self._name = name
def __set__(self, instance, value):
if not isinstance(value, self._expected_type):
raise TypeError(
f"Attribut '{self._name}' : type attendu {self._expected_type.__name__}, "
f"reçu {type(value).__name__}"
)
instance.__dict__[self._name] = value
# Descripteurs typés
class Integer(Typed): _expected_type = int
class Float(Typed): _expected_type = float
class String(Typed): _expected_type = str
class OrderedMeta(type):
def __new__(cls, clsname, bases, clsdict):
d = dict(clsdict)
ordre = []
for name, value in clsdict.items():
if isinstance(value, Typed):
value._name = name
ordre.append(name)
d['_order'] = ordre
return type.__new__(cls, clsname, bases, d)
class Etudiant(metaclass=OrderedMeta):
nom = String()
prenom = String()
age = Integer()
note = Float()
def __init__(self, nom, prenom, age, note):
self.nom = nom
self.prenom = prenom
self.age = age
self.note = note
# ✅ Types corrects
s1 = Etudiant('ESSADDOUKI', 'Mostafa', 31, 13.5)
print("Ordre des attributs :", s1._order)
# ❌ age est une chaîne au lieu d'un entier
s2 = Etudiant('Kayouh', 'Mohamed', "30", 14.0)Ordre des attributs : ['nom', 'prenom', 'age', 'note'] TypeError: Attribut 'age' : type attendu int, reçu str
Quand utiliser les métaclasses ?
| Situation | Métaclasse justifiée ? | Alternative possible |
|---|---|---|
| Contrôle propagé dans toute une hiérarchie | ✅ Oui | — |
| Modification automatique à la création d'une classe | ✅ Oui | Décorateur de classe |
| Développement d'une API / framework | ✅ Oui | — |
| Validation de types ou d'attributs | ⚠️ Parfois | __init_subclass__, dataclasses |
| Ajouter un comportement à une seule fonction | ❌ Non | Décorateur simple |