IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Python, de zéro


précédentsommairesuivant

XVIII. Les métaclasses

Les métaclasses sont une notion assez complexe qui fait appel à un très haut degré d’abstraction. Et pour 99% des utilisateurs, ne seront jamais utiles. Mais elles existent donc…

Pour résumer brièvement, si une classe sert à créer un objet, alors une métaclasse sert à créer une classe.

Ainsi si UnObjet=UneClasse() alors UneClasse=UneMetaClass().

XVIII-1. La fonction « type() »

On sait que tout objet est instance d’une classe. On dit aussi que la classe est le type de l’objet. Et donc, tout objet a forcément un type.

Le type d’un objet peut être récupéré grâce à la fonction type().

 
Sélectionnez
1.
2.
3.
4.
5.
6.
>>> type(5)
<class 'int'>
>>> type("Hello")
<class 'str'>
>>> type(lambda: None)
<class 'function'>

Mais si les classes sont des objets, quel est alors leur type ?

 
Sélectionnez
1.
2.
>>> type(object)
<class 'type'>

Ainsi, le type d’un objet est un… type. On ne peut pas faire plus simple comme définition !

Or, on a vu depuis le début que tout type Python possède une fonction dédiée permettant de le créer (il existe un type int ainsi qu’une fonction int() permettant de créer un int ; il existe un type float ainsi qu’une fonction float() permettant de créer un float ; il existe un type tuple ainsi qu’une fonction tuple() permettant de créer un tuple ; etc.). Donc, de même puisqu’il existe un type type, il existe aussi une fonction type() permettant de créer un type c’est-à-dire un objet.

Pour créer un objet par cette fonction, on doit lui passer le nom de la classe à créer (évident), un tuple contenant le ou les objets dont il veut hériter s’il doit hériter (logique) et un dictionnaire analogue à l’attribut __dict__ c’est-à-dire contenant les attributs que l’on veut avoir dans l’objet et leurs valeurs.

Exemple :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
>>> v=type("vehicule", (), {"vMax" : 60})
>>> print(v)
<class '__main__.vehicule'>
>>> print(v.vMax)
60
>>> vv=type("voiture", (v,), {"marque" : "lada"})
>>> print(vv)
<class '__main__.voiture'>
>>> print(vv.vMax)
60
>>> print(vv.marque)
'lada'

En fait, ceci est le comportement naturel de la fonction type() qui a pour but officiel de créer un objet. Son second comportement (retourner le type de l’objet qu’on lui passe en paramètre) qu’on a vu depuis le début n’est qu’un comportement particulier.

Et puisque la fonction type() permet de créer un objet (en particulier aussi une classe), alors c’est le premier exemple d’une métaclasse. En fait, c’est la métaclasse de base dont Python se sert pour créer toutes les classes quand il traite l’instruction class.

Le même exemple écrit sous forme classique :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
>>> class vehicule:
...    def __init__(self, vMax):
...        self.vMax=vMax
...
>>> v=vehicule(60)
>>> print(v)
<class '__main__.vehicule'>
>>> print(v.vMax)
60
>>> class voiture(vehicule):
...    def __init__(self,  marque, vMax):
...        super().__init__(vMax)
...        self.vmarque=marque
...
>>> vv=voiture("Lada", 60)
>>> print(vv)
<class '__main__.voiture'>
>>> print(vv.vMax)
60
>>> print(vv.marque)
'lada'

XVIII-2. Créer sa propre fonction génératrice

Puisque les fonctions peuvent retourner des objets, elles peuvent aussi retourner des classes.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
def makeClass(nom):
    if nom == "carre":
        class carre:
            def __init__(self, cote): self.cote=cote
            def surface(self): return self.cote ** 2
        return carre                     # Attention pas de parenthèses, on retourne un type !!!
    elif nom == "cercle":
        class cercle:
            def __init__(self, rayon):
                self.rayon=rayon
            def surface(self): return self.rayon ** 2 * 3.14
        return cercle                    # Attention pas de parenthèses, on retourne un type !!!
    else:
        return None

>>> cFigure=makeClass("cercle")          # cFigure est une classe "cercle"
>>> cFigure, type(cFigure)
<class '__main__.makeClass.<locals>.cercle'> <class 'type'>
>>> figure=cFigure(5)                    # Je peux alors instancier un objet cercle
>>> figure
<__main__.makeClass.<locals>.cercle object at 0x7fea924297f0>
>>> figure.surface()
78.5
>>>
>>> cFigure=makeClass("carre")           # cFigure est une classe "carre"
>>> cFigure, type(cFigure)
<class '__main__.makeClass.<locals>.carre'> <class 'type'>
>>> figure=cFigure(5)                    # Je peux alors instancier un objet carre
>>> figure
<__main__.makeClass.<locals>.carre object at 0x7fea92429860>
>>> figure.surface()
25

XVIII-3. Puis passer par une classe

Et puisqu’une classe peut aussi faire des trucs au travers de ses méthodes (qui sont des fonctions), alors elle peut aussi créer une classe. Ne reste qu’à découvrir sa syntaxe.

Tout d’abord, qu’est-ce qu’une classe dans Python ? C’est juste un enrobage d’un simple dictionnaire : le __dict__(). C’est lui qui contient la liste des attributs et méthodes de la classe. Ainsi, créer une classe, c’est simplement remplir (ou juste modifier) ces attributs (ce __dict__()).

Nous allons donc créer une fonction qui recevra une classe en paramètre et qui modifiera les attributs en leur rajoutant (pour l’exemple) une chaîne correspondant au nom de la classe. Ainsi, pour la classe toto, l’attribut xxx deviendra toto_xxx (oui, pas vraiment utile, mais c’est juste pour l’exemple !).

Cette fonction ayant le même comportement que la fonction type(), elle doit avoir une signature identique, c’est-à-dire recevoir le nom de la classe, les ancêtres et les attributs.

Pour l’exemple, cette fonction qui servira à personnaliser notre classe s’appellera custom().

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
def custom(cls, parents, attr):
    # Un petit affichage préalable pour montrer ce qui se passe…
    print("custom: cls=%s, parents=%s, attr=%s" % (cls, parents, attr))

    # Elle commence par définir la chaîne à rajouter en fonction du nom de la classe
    keyword={
        "woody" : "cowboy",
        "buzz" : "eclair",
    }.get(cls)

    # Ensuite elle crée le dictionnaire des nouveaux attributs
    nouveaux_attr=dict()

    # Puis elle traite les attributs de la classe reçue en paramètre
    for (k, v) in attr.items():
        # S’il y a un mot clef (classe particulière) et que l’attribut n’est pas spécial
        # Le mot à rajouter est créé sinon il reste vide
        word="%s_" % keyword if keyword is not None and not k.startswith("__") else ""

        # L’attribut est copié dans le dictionnaire avec son nouvel identificateur
        nouveaux_attr[word + k]=v

    # La fonction retourne une nouvelle classe créée dynamiquement depuis la classe reçue
    return type(cls, parents, nouveaux_attr)

Maintenant, nous écrivons nos classes en leur indiquant qu’elles sont métaclassées par la fonction custom. Cela se fait en écrivant explicitement metaclass=nom_de_la_fonction entre parenthèses après le nom de la classe.

Attention, le message qui est indiqué après chaque classe dans cet exemple n’est pas à écrire, il provient du print() situé au début de la fonction custom()

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
>>> class woody(metaclass=custom):
...    def action(self):
...        print("Il y a un serpent dans ma botte !!!")
...    def dodo(self):
...        print("wwwwww")
...
custom: cls=woody, parents=(), attr={'action': <function action at 0x7fc0e75b7758>, 'dodo': <function dodo at 0x7fc0e75b77d0>, '__module__': 'meta', '__metaclass__': <function custom at 0x7fc0e75b7668>}
>>>
>>> class buzz(metaclass=custom):
...    def action(self):
...        print("Vers l'infini et au delà !!!")
...    def dodo(self):
...        print("zzzzzz")
...
custom: cls=buzz, parents=(), attr={'action': <function action at 0x7fc0e75b7848>, 'dodo': <function dodo at 0x7fc0e75b78c0>, '__module__': 'meta', '__metaclass__': <function custom at 0x7fc0e75b7668>}

À noter : sous Python 2, la métaclasse se crée par le biais d’un attribut statique particulier __metaclass__. Là aussi apparaîtra évidemment le même message.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
# Spécifique Python 2
>>> class woody:
...    __metaclass__=custom
...    def action(self):
...        print("Il y a un serpent dans ma botte !!!")
...    def dodo(self):
...        print("wwwwww")
...
custom: cls=woody, parents=(), attr={'action': <function action at 0x7fc0e75b7758>, 'dodo': <function dodo at 0x7fc0e75b77d0>, '__module__': 'meta', '__metaclass__': <function custom at 0x7fc0e75b7668>}
>>>
>>> class buzz:
...    __metaclass__=custom
...    def action(self):
...        print("Vers l'infini et au delà !!!")
...    def dodo(self):
...        print("zzzzzz")
...
custom: cls=buzz, parents=(), attr={'action': <function action at 0x7fc0e75b7848>, 'dodo': <function dodo at 0x7fc0e75b78c0>, '__module__': 'meta', '__metaclass__': <function custom at 0x7fc0e75b7668>}

Ensuite, il ne reste qu’à instancier nos classes et les tester.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
>>> w=woody()
>>> dir(w)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'cowboy_action', 'cowboy_dodo']

>>> b=buzz()
>>> dir(b)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'eclair_action', 'eclair_dodo']

Chaque méthode action() et dodo() de chaque objet a bien été renommée en fonction du nom de l’objet lui‑même. On peut donc les utiliser par leur nouveau nom.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
>>> w.cowboy_action()
Il y a un serpent dans ma botte !!!
>>> w.cowboy_dodo()
wwwwww
>>> b.eclair_action()
Vers l’infini et au-delà !
>>> b.eclair_dodo()
zzzzzz

Et bien évidemment, les méthodes originelles action() et dodo() définies par le programmeur des classes woody et buzz n’existent plus puisque la fonction custom() a changé leurs identificateurs.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
>>> w=woody()
>>> w.action()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'woody' object has no attribute 'action'
>>> w.dodo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'woody' object has no attribute 'dodo'
>>>
>>> b=buzz()
>>> b.action()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'buzz' object has no attribute 'action'
>>> b.dodo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'buzz' object has no attribute 'dodo'

XVIII-4. Code complet

Voici le code complet directement exécutable de ce chapitre.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
#!/usr/bin/env python3
# coding: utf-8

# La fonction de customisation
def custom(cls, parents, attr):
    # Un petit affichage préalable pour montrer ce qui se passe…
    print("custom: cls=%s, parents=%s, attr=%s" % (cls, parents, attr))

    # Elle commence par définir la chaîne à rajouter en fonction du nom de la classe
    keyword={
        "woody" : "cowboy",
        "buzz" : "eclair",
    }.get(cls)

    # Ensuite elle crée le dictionnaire des nouveaux attributs
    nouveaux_attr=dict()

    # Puis elle traite les attributs de la classe reçue en paramètre
    for (k, v) in attr.items():
        # S’il y a un mot clef (classe particulière) et que l’attribut n’est pas spécial
        # Le mot à rajouter est créé sinon il reste vide
        word="%s_" % keyword if keyword is not None and not k.startswith("__") else ""

        # L’attribut est copié dans le dictionnaire avec son nouvel identificateur
        nouveaux_attr[word + k]=v
    # for

    # La fonction retourne une nouvelle classe créée dynamiquement depuis la classe reçue
    return type(cls, parents, nouveaux_attr)
# custom()

# L’objet "woody" qui sera customisé par la fonction
class woody(metaclass=custom):
    def action(self): 
        print("Il y a un serpent dans ma botte !")
    def dodo(self):
        print("wwwwww")
# class woody

# L’objet "buzz" qui sera customisé par la fonction
class buzz(metaclass=custom):
    def action(self):
        print("Vers l'infini et au-delà !")
    def dodo(self):
        print("zzzzzz")
# class buzz

# Test de l’objet "woody"
w=woody()
w.cowboy_action()
w.cowboy_dodo()

# Test de l’objet "buzz"
b=buzz()
b.eclair_action()
b.eclair_dodo()

XVIII-5. Pour finir

Si vous vous demandez si vous avez besoin des métaclasses, alors ce n’est pas le cas. Les gens qui en ont vraiment besoin le savent avec certitude et n’ont pas besoin d’explications sur la raison (Tim Peters).


précédentsommairesuivant

Copyright © 2022 Svear (svear@free.fr) Permission est accordée de copier, distribuer ou modifier ce document selon les termes de la « Licence de Documentation Libre GNU » (GNU Free Documentation License), version 1.1 ou toute version ultérieure publiée par la Free Software Foundation.