Les Crash-Devs d'un Passionné

Utiliser la classe metaclass

/Catégorie/python

Temps de lecture : 2 minutes

Une métaclasse en python permet de définir le comportement de l'objet enfant sur lequel il est défini. On retrouve la spécification sur la PEP 3115

Si on prend une classe vide et que l'on analyse son type

clipboard
Copier le code
1
2
3
4
5
class MyClass():
   pass

type(MyClass)
<class 'type'>

On remarque que MyClass est de type

type

Il hérite de la métaclasse type. Donc pour créer une nouvelle métaclasse on fait

clipboard
Copier le code
1
2
3
4
5
class MyMeta(type):
   pass

type(MyMeta)
<class 'type'>

Et pour appliquer cette metaclasse à une classe on la déclare ainsi

clipboard
Copier le code
1
2
3
4
5
class MyClass(metaclass=MyMeta):
   pass

type(MyClass)
<class '__main__.MyMeta'>

metaclass permet d'agir sur le dunder __new__ qui est appelé avant la création de l'objet.

Nous avons également vu qu'il doit hériter de type qui prend 3 paramètres :

  • name: nom de la classe
  • bases: tuple des classes parentes
  • attrs: dictionaire contenant les noms et valeurs des attributs

Donc si on veut simuler la création de notre instance MyClass depuis la métaclasse on fait

return super().__new__(cls, name, bases, dict)

Ce qui nous donnerait

clipboard
Copier le code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class MyMeta(type):
     def __new__(cls, name, bases, attrs):
         return super().__new__(cls, name, bases, attrs)
 
class MyClass(metaclass=MyMeta):
     pass
 
>>> type(MyMeta)
<class 'type'>
>>> type(MyClass)
<class '__main__.MyMeta'>

Jusque là rien de bien nouveau. Mais si on veut modifier notre instance en ajoutant par exemple un attribut qui correspond à la date de création, on ferait

clipboard
Copier le code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from datetime import datetime

class MyMeta(type):
   def __new__(cls, name, bases, attrs):
    obj = super().__new__(cls, name, bases, attrs)
    obj.created_date = datetime.now()
    return obj

class MyClass(metaclass=MyMeta):
    pass


>>> type(MyMeta)
<class 'type'>
>>> type(MyClass)
<class '__main__.MyMeta'>
>>> print(MyClass().created_date)
2022-09-07 14:00:07.867773

Ce mécanisme pourrait très bien être implémenté avec de l'héritage simple, c'est à vous de déterminer ce qui est le plus adapté en fonction de votre besoin et du contexte.

On pourrait très bien imaginer une métaclasse qui associe un attribut à un logger

clipboard
Copier le code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import logging

class MyMeta(type):
   def __new__(cls, name, bases, attrs):
      obj = super().__new__(cls, name, bases, attrs)
      obj.log = logging.getLogger()
      return obj

class MyClass(metaclass=MyMeta):
   def __init__(self):
      self.log.warning('Instance created...')


MyClass() 
>>> Instance created...

On vient de voir comment type permet de créer des instances de classe et comment vous pouvez vous aussi personnaliser ces objets