Les métaclasses en Python et Django

Ces notes sont une simple reformulation d'un article de James Bennett en guise de mémo.

type ou la class des classes

Voici une explication haut niveau du fonctionnement des classes en Python.

Tout ce qui se trouve à l'intérieur du bloc de code d'une classe est exécuté par Python qui en tire un dictionnaire représentant l'espace de noms local de la classe.

Par exemple, pour la classe suivante…

import math

class Circle:
    def __init__(self, center, radius):
        self.center = center
        self.radius = radius

    def area(self):
        return math.pi * (self.radius ** 2)

    def circumference(self):
        return 2 * math.pi * self.radius

…Python va produire un dictionnaire contenant trois clés :

namespace_dict = {
    "__init__": <function…>,
    "area":  <function…>,
    "circumference":  <function…>,
}

Puis Python rassemble 3 choses…

  • le nom donné dans la déclaration de classe initiale (Circle)
  • les classes parentes (le cas échéant)
  • le dictionnaire représentant l'espace de noms de la classe

…qu'il passe à type() qui n'est pas une fonction mais une classe (la class des classes) :

Circle = type("Circle", (), namespace_dict)

En résumé, le processus de définition de la classe Circle consiste à :

  1. exécuter le corps de la classe et rassembler tout ce qui s'y trouve dans un dictionnaire
  2. appeler type("Circle", (), namespace_dict)
  3. affecter le résultat au nom Circle

Les métaclasses

Les métaclasses permettent de se greffer dans le processus décrit ci-dessus pour modifier la façon dont les classes sont construites.

En Python, __init__ n'est pas un constructeur. La création d'une instance de classe Python est en réalité un processus en deux étapes :

  • un appel à __new__() (une méthode statique qui crée des instances)
  • puis un appel à __init__()

Pour c = Circle(center=(0, 0), radius=1), Python fait à peu près ça :

c = Circle.__new__(Circle, center=(0, 0), radius=1)
c.__init__(center=(0, 0), radius=1)

Puisque la définition d'une nouvelle classe implique la création d'une nouvelle instance de type, on passera forcément par type.__new__().

Le mécanisme de Python pour personnaliser la création des classes est basé là dessus et consiste à :

  • hériter de type dans une sous-classe
  • y surcharger __new__()
  • puis à dire à Python d'utiliser cette sous-classe

Une classe qui fait ça est une métaclasse :

>>> class SimpleMetaclass(type):
...     def __new__(cls, name, bases, attrs):
...         attrs["special_attribute"] = "Special!"
...         return super().__new__(cls, name, bases, attrs)
...
>>> class SpecialClass(metaclass=SimpleMetaclass):
...     pass
...
>>> SpecialClass.special_attribute
"Special!"

Ce mécanisme est utile pour mettre en place automatiquement (de façon quasi "magique") des attributs, des méthodes et des comportements dans une classe.

Les inconvénients des métaclasses :

  • ils rendent le code plus difficile à comprendre car des choses apparaissent ou changent sans raison (dans une hiérarchie de classes, on a pas vu qu'une classe parente hérite d'une métaclasse)
  • le système d'héritage peut devenir complexe quand une classe a plusieurs parents qui utilisent des métaclasses différentes, cf. l'erreur "The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases" avec des conflits compliqués voire impossibles à résoudre

contribute_to_class() dans Django

contribute_to_class() permet d'attacher des choses à une classe de modèle de l'ORM de Django.

Pour comprendre comment elle est utilisée, il faut se rendre compte que django.db.models.Model utilise la métaclasse django.db.models.base.ModelBase.

Lorsque ModelBase passe en revue le dictionnaire de la classe du modèle qu'elle construit, elle vérifie chaque attribut pour voir si sa valeur est une méthode nommée contribute_to_class().

Si c'est le cas, elle l'appelle en lui passant la classe du modèle qu'elle construit et le nom de l'attribut.

contribute_to_class() est très utilisée pour attacher des Fields ou des Managers aux modèles Django. La méthode contribute_to_class() de DateField attache les méthodes next et previous etc.

contribute_to_class() est une API interne, privée et non documentée de Django. Il n'y a pas de garantie de rétrocompatibilité.

Pour aller plus loin

Avant Encapsuler la complexité avec l'ORM de Django Après Coder chez Google - Management

Tag Kemar Joint