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(
).
Mais si les classes sont des objets, quel est alors leur 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 :
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 :
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.
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
(
).
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
(
)…
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.
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.
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.
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.
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.
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).