Métaprogrammation en Python

25 Apr 2019 25 Apr 2019 10017 vues ESSADDOUKI Mostafa

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.

Définition — Métaprogrammation La métaprogrammation consiste à écrire du code dont le but principal est de manipuler d'autre code : le modifier, le générer ou l'encapsuler. En Python, ses principales formes sont les décorateurs, les décorateurs de classe et les métaclasses.
OutilNiveau d'actionUsage typique
DécorateursFonctions / méthodesAjouter un comportement sans modifier la fonction
Décorateurs de classeClassesModifier une classe après sa définition
MétaclassesCréation des classes elles-mêmesContrô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))    # 
Sortie
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)) # 
Sortie
Type de p        : <class '__main__.Personne'>
Type de Personne : <class 'type'>
Définition — Métaclasse Une métaclasse est la classe d'une classe. Tout comme un objet est une instance d'une classe, une classe est une instance d'une métaclasse. La métaclasse par défaut de Python est type, qui est responsable de la création de toutes les classes.
Hiérarchie de création

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 : ESSADDOUKI
Sortie
Nom : ESSADDOUKI

Créer une classe avec type()

La fonction type() peut être appelée de deux façons différentes :

AppelArgumentsRésultat
type(objet)1 argumentRetourne le type de l'objet
type(nom, bases, dict)3 argumentsCrée et retourne une nouvelle classe

Syntaxe — Création dynamique de classe Python
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)
Sortie
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éthodeRôleParamè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éationIdem

Syntaxe — Métaclasse personnalisée Python
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):
    pass
Sortie
Traceback (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()
Sortie
TypeError: Impossible d'instancier directement 'Singleton'
Cas d'usage réel — Patron Singleton En redéfinissant __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)
Sortie
Ordre des attributs : ['nom', 'prenom', 'age', 'note']

TypeError: Attribut 'age' : type attendu int, reçu str

Quand utiliser les métaclasses ?

Règle générale Les métaclasses sont souvent décrites comme de la « magie noire ». Dans la majorité des cas, un décorateur ou un héritage simple suffisent. Recourez aux métaclasses uniquement lorsque les autres approches ne sont pas envisageables.
SituationMétaclasse justifiée ?Alternative possible
Contrôle propagé dans toute une hiérarchie✅ Oui
Modification automatique à la création d'une classe✅ OuiDé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❌ NonDécorateur simple