I. Introduction▲
Lorsque l'on apprend à coder en Python, on monte progressivement et rapidement en puissance sur ce langage.
Mais lorsqu'on atteint la programmation objet, certaines manipulations peuvent paraître floues. Ainsi, comment faire pour pouvoir écrire une classe dérivant d'une autre, afin d'éviter de multiples copies ?
Une fois de plus, Python, comme d'autres langages orientés objet, possède la réponse à cela : l'héritage.
Faisant partie des concepts objet relativement avancés, et de Python, l'héritage, et par extension les métaclasses, sont des notions à connaître.
Grâce à ces concepts, Python va nous permettre d'écrire du code propre, sans redondance, et bien structuré. En d'autres termes, du code efficace et aisément maintenable, donc pérenne.
II. Prérequis▲
Afin de pouvoir lire sans soucis ce tutoriel, il vous faudra auparavant disposer des prérequis suivants :
- être à l'aise avec les notions de packages et modules ;
- maîtriser les notions liées à la programmation objet (objet, classe, attribut…) ;
- savoir créer une classe simple.
III. L'héritage▲
III-A. Principe▲
Afin d'illustrer au mieux l'héritage, je vous propose de passer par un exemple.
Pour définir un tabouret, nous pouvons dire qu'il possède au minimum trois pieds et une assise.
De même, nous pouvons définir une chaise en stipulant qu'elle possède au minimum trois pieds, une assise, et un dossier. Il y a des points communs non ? Une chaise n'est rien d'autre qu'un tabouret avec un dossier.
Si nous remettons tout cela dans un contexte informatique, les caractéristiques permettant de définir un tabouret ou une chaise sont assimilables à des attributs. Nous pouvons donc définir une classe « tabouret » et une classe « chaise ».
Cependant, plutôt que d'écrire du code en doublon dans notre classe « chaise », nous allons la faire hériter de la classe « tabouret », puis lui ajouter un attribut dossier.
Ainsi, quand nous allons créer notre classe « chaise », celle-ci va hériter de la classe « tabouret ». Nous dirons alors que la classe « tabouret » est la « classe mère », et la classe « chaise », la « classe fille ».
Quand nous créons notre classe, nous allons lui passer en paramètre le nom de la classe dont nous voulons hériter. Puis, dans notre constructeur, nous allons initialiser le constructeur de notre classe mère avant d'initialiser notre propre classe.
Une fois cela fait, rien ne vous empêche alors de modifier des attributs ou des fonctions en les surchargeant. Par exemple, imaginons que votre classe « tabouret » possède un attribut « nombre_de_pied » paramétré à 3. Dans votre classe chaise, vous pourrez alors surcharger cet attribut en lui assignant la valeur 4.
La logique de la programmation objet veut que si un élément (attribut ou fonction) existe à la fois dans la classe mère et dans la classe fille, c'est l'élément de la classe fille qui est pris en compte quand on utilise un objet de cette dernière. C'est cela qu'on appelle la « surcharge ».
III-B. Héritage simple▲
III-B-1. Déclaration de l'héritage▲
Pour stipuler la classe mère, rien de bien compliqué. Cela se passe à la définition de la classe fille :
…
class
MaClasseFille
(
MaClasseMere):
…
III-B-2. Initialiser la classe mère▲
Une fois la classe mère définie, il va vous falloir l'initialiser. Pour cela, il suffit d'appeler le constructeur de la classe mère dans le constructeur de la classe fille :
…
class
MaClasseFille
(
MaClasseMere):
def
__init__
(
self):
MaClasseMere.__init__
(
self)
…
Veuillez noter qu'en Python 3.X, vous pouvez simplement utiliser la syntaxe suivante :
…
class
MaClasseFille
(
MaClasseMere):
def
__init__
(
self):
super(
).__init__
(
)
…
Ici, super() est une référence implicite à la classe mère (le terme super-classe est un synonyme de classe mère). À ce titre, cela vous dispense d'utiliser le self lors de l'appel. Cependant, si vous désirez rendre votre code compatible toute branche, la syntaxe standard est préférable.
III-B-3. Surcharger un attribut▲
Surcharger un attribut est très simple, puisqu'il suffit de redéfinir sa valeur au sein de la classe fille.
…
class
MaClasseFille
(
MaClasseMere):
def
__init__
(
self):
MaClasseMere.__init__
(
self)
self.attribut_classe_mere =
'nouvelle valeur'
III-B-4. Surcharger une fonction▲
Tout comme pour les attributs, pour surcharger une fonction, il suffit de la redéfinir.
Rien n'empêche l'élément de la classe fille de faire appel à l'élément de la classe mère.
III-C. Héritage multiple▲
L'héritage multiple repose sur les mêmes principes que l'héritage simple. Cependant, au lieu de stipuler une seule classe mère, nous allons en indiquer plusieurs. Et, bien entendu, il faut initialiser chaque classe dont on hérite.
Pour rendre cela plus compréhensible, je vous propose de prendre un nouvel exemple.
Imaginons que nous possédions deux classes. La première contient les caractéristiques génériques d'une voiture (une voiture possède des roues, un moteur…). La seconde contient les caractéristiques typiques d'une marque donnée (ex. : Citroën et ses suspensions hydractives).
On pourrait imaginer créer une classe pour chaque modèle de la marque. Chaque modèle étant à la fois une voiture, et une Citroën, les classes de chaque modèle de la marque pourraient alors hériter des deux classes.
…
class
Bx
(
Voiture, Citroen):
def
__init__
(
self):
Voiture.__init__
(
self)
Citroen.__init__
(
self)
...
Il y a une chose à laquelle il faut faire attention avec les héritages multiples : les noms d'attributs ou de méthodes communs à plusieurs classes de l'arbre d'héritage. En effet, au mieux Python vous indiquera un conflit, au pire il en choisira un au hasard parmi les disponibles.
S'il est tout à fait possible que vous voyiez des héritages multiples au cours de vos lectures de sources, sachez cependant qu'il s'agit d'un concept assez peu répandu.
III-D. Le MRO▲
III-D-1. Théorie▲
Le MRO pour Method Resolution Order est la fonctionnalité des langages objet à héritage multiple qui permet de déterminer dans quel ordre effectuer un héritage multiple.
Dans les cas basiques, tels que dans l'exemple pratique qui suivra, tout se passe naturellement, sans rien avoir à prendre en compte. Tout sera donc transparent pour vous.
Cependant, dans d'autres cas autrement plus complexes, le MRO entrera en jeu et il faudra en tenir compte.
Le MRO repose sur un algorithme qui a été modifié dans Python 2.3. Dans un souci de rétrocompatibilité, les deux algorithmes ont vécu côte à côte, dans la branche 2. La distinction se fait alors lors de la déclaration d'une classe
2.
3.
4.
5.
6.
7.
8.
class
A
(
):
pass
class
B
(
):
pass
class
C
(
A, B):
pass
Comme vous pouvez le constater, la différence se fait lors de la déclaration des classes mères. Si on explicite qu'on désire hériter de la classe « object », alors on fait systématiquement appel au nouvel algorithme lors de la création de la classe C. Dans le cas contraire, Python utilisera d'office l'ancien algorithme.
Cependant, dans la branche 3, c'est systématiquement le nouvel algorithme qui est utilisé, que l'on explicite « object » ou non.
Dans ce tutoriel, seul le nouvel algorithme sera considéré.
Il est recommandé désormais, en cas d'héritage multiple, de systématiquement stipuler « object », afin que le code puisse être exécuté indifféremment en branche 2 ou 3 de Python. Le cas échéant, le code pourrait avoir un comportement différent selon la branche d'exécution.
En effet, si la nouvelle classe hérite de deux classes, et que chacune des classes mères possède une méthode du même nom, mais au fonctionnement différent, la surcharge n'aura pas le même résultat.
III-D-2. Connaître l'ordre d'héritage▲
Afin de pouvoir déboguer lors d'erreurs avec l'héritage multiple, il est possible de connaître l'ordre d'héritage. Pour cela, on utilise la méthode « __mro__ ».
L'exécution de ce code vous renverra
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <type 'object'>)
Comme on peut le voir, on retrouve d'abord la classe qu'on interroge, notre classe C, puis on voit ensuite qu'on hérite de la classe A avant d'hériter de la classe B.
Ce cas est fort simple, et pour hériter à l'inverse, il suffirait d'inverser B et A dans l'ordre d'héritage. Mais cela n'est pas toujours aussi simple.
III-D-3. Exemple▲
L'exemple que je vais vous proposer ici est inspiré de celui de la documentation officielle (voir le lien dans l'encadré suivant).
Nous allons effectuer des héritages multiples plus ou moins complexes qui vous montreront l'utilité de pouvoir lire l'ordre d'héritage.
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.
class
A
(
object):
pass
class
B
(
object):
pass
class
C
(
A, B):
pass
class
D
(
B,A):
pass
class
E
(
D,A):
pass
try
:
class
F
(
A,D):
pass
except
Exception
as
e:
print
(
"Voici l'erreur a la creation de la classe F"
)
print
(
e)
if
__name__
==
'__main__'
:
print
(
"
\n
Voici l'execution du main"
)
print
(
C.__mro__
)
print
(
D.__mro__
)
print
(
E.__mro__
)
Je vous invite à exécuter ce code. Le résultat est alors le suivant :
2.
3.
4.
5.
6.
7.
8.
Voici l'erreur a la creation de la classe F
Error when calling the metaclass bases
Cannot create a consistent method resolution order (MRO) for bases D, A
Voici l'execution du main
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <type 'object'>)
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.A'>, <type 'object'>)
(<class '__main__.E'>, <class '__main__.D'>, <class '__main__.B'>, <class '__main__.A'>, <type 'object'>)
Passons tout d'abord au résultat du main. Aucun souci. Nous voyons, comme vu précédemment l'ordre d'héritage.
Cela peut s'avérer utile lorsque vous utilisez une classe, sans savoir de quelles autres classes elle peut hériter.
Mais penchons-nous plus avant sur l'erreur générée au début du code. On voit qu'il est fait question de « métaclasses ». Les explications à ce sujet sont dans le chapitre suivant. Nous ferons donc l'impasse à ce sujet dans l'immédiat.
Ce qu'il faut regarder plus précisément c'est l'erreur « Cannot create a consistent method resolution order (MRO) for bases D, A ». En réalité, ce qu'il se passe, c'est que le MRO a détecté un problème d'héritage.
Si on essaie de reproduire ce que fait le MRO, cela donnerait :
(<class '__main__.F'>, <class '__main__.A'>, <class '__main__.D'>, <class '__main__.B'>, <class '__main__.A'>)
Si on compare avec la classe E qui hérite en sens inverse :
(<class '__main__.E'>, <class '__main__.D'>, <class '__main__.B'>, <class '__main__.A'>, <type 'object'>)
Pour la classe E, on hérite deux fois de la classe A, donc la surcharge n'aura aucun effet. Le MRO l'a bien compris, et ne réalise finalement qu'un seul héritage.
Pour la classe F, on hérite aussi deux fois de la classe A. Mais entre ces deux héritages, la classe F hérite de diverses classes. Le MRO trouve cela bizarre et refuse donc la construction de la classe, et affiche un message d'erreur.
Si vous désirez approfondir le sujet, outre le lien donné au début de l'exemple, voici des liens qui vous seront très utiles.
http://makina-corpus.com/blog/metier/2014/python-tutorial-understanding-python-mro-class-search-path
https://docs.python.org/2/library/stdtypes.html#class.__mro __https://docs.python.org/2/library/stdtypes.html#class.__mro__
https://www.python.org/download/releases/2.3/mro/https://www.python.org/download/releases/2.3/mro/
III-E. Exemple pratique▲
Après avoir vu l'ensemble des bases , je vous propose ici de mettre tout cela en œuvre, à travers un exemple pratique : les voitures.
En effet, de base, nous pouvons considérer qu'une voiture possède des roues, un volant, un fauteuil conducteur et un moteur. Nous allons donc créer notre classe mère selon ce principe.
Maintenant nous allons créer plusieurs classes correspondant à différents modèles de voiture, héritant bien entendu de notre classe mère « voiture ».
Les voitures retenues pour cela sont :
- La Bond Bug
- La Citroën DS 1967
Ces images sont issues des sites Wikipédia
Ces modèles vont nous permettre d'agir au niveau du moteur, des roues, et des fauteuils.
Voici la structure que nous allons suivre :
Nous allons ici créer un package « voiture », contenant un module avec notre classe mère, puis un sous-package par marque, contenant chacun un module par modèle de voiture. Nous pouvons fort bien imaginer que ce module contienne l'ensemble des phases, c'est-à-dire des déclinaisons, du même modèle.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
class
Voiture
(
):
def
__init__
(
self):
self.nombre_roues =
4
self.nombre_fauteuils =
1
self.moteur =
False
self.volant =
True
def
start_moteur
(
self):
self.moteur =
True
return
self.moteur
def
stop_moteur
(
self):
self.moteur =
False
return
self.moteur
def
statut_moteur
(
self):
return
self.moteur
if
__name__
==
"__main__"
:
ma_voiture_basique =
Voiture
(
)
print
(
ma_voiture_basique.statut_moteur
(
))
ma_voiture_basique.start_moteur
(
)
print
(
ma_voiture_basique.statut_moteur
(
))
Dans le code de notre classe originelle, rien d'extraordinaire. Nous avons ajouté quelques fonctions basiques pour la suite.
2.
3.
4.
5.
6.
7.
8.
9.
10.
class
Citroen
(
):
def
__init__
(
self):
self.type_suspension =
"Hydractives"
self.logo =
"Chevrons"
self.marque =
"Citroen"
if
__name__
==
"__main__"
:
ma_citroen =
Citroen
(
)
print
(
ma_citroen.type_suspension)
print
(
ma_citroen.logo)
Dans cette classe nous définissons les caractéristiques d'une voiture Citroën : son logo, ses suspensions typiques (du moins jusqu'il y a peu) et le nom de la marque.
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.
from
voiture import
Voiture
class
BondBug
(
Voiture):
def
__init__
(
self):
Voiture.__init__
(
self)
self.marque =
"Bond"
self.modele =
"Bug"
self.nombre_roues =
3
self.nombre_fauteuils =
2
print
(
self.statut_moteur
(
))
def
start_moteur
(
self):
# Sur la ligne suivante de code, si nous ne stipulons pas expressement qu'on adresse
# l'attribut de Voiture, Python comprendra qu'on fait reference a BondBug.start_moteur
# a cause de la surcharge. Le self est la car nous appelons l'objet concerne de la classe
# mere
state =
Voiture.start_moteur
(
self)
print
(
"Statut du moteur:
%s
"
%
(
state))
def
stop_moteur
(
self):
state =
Voiture.stop_moteur
(
self)
print
(
"Statut du moteur:
%s
"
%
(
state))
if
__name__
==
"__main__"
:
my_bug =
BondBug
(
)
my_bug.start_moteur
(
)
my_bug.stop_moteur
(
)
La Bond Bug est une voiture anglaise caractéristique. En effet, pour entrer dans cette dernière, il faut lever la carrosserie, enfin une partie, qui est en fibre. Sa grande particularité est qu'elle ne possède que trois roues, et deux fauteuils.
Dans notre classe, nous avons donc redéfini, par surcharge, le nombre de roues et de fauteuils. Outre cela, nous avons surchargé les méthodes liées au moteur.
Le commentaire met en évidence l'attention qu'il faut porter en cas de surcharge de méthode. Dans notre cas, si au lieu de noter « Voiture.start_moteur(self) », nous avions écrit « self.start_moteur() », le code serait tombé dans une boucle récursive. Je vous invite à essayer afin de mieux comprendre le principe.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
from
voiture import
Voiture
from
citroen import
Citroen
class
CitroenDs
(
Voiture, Citroen):
def
__init__
(
self):
Voiture.__init__
(
self)
Citroen.__init__
(
self)
self.modele =
"DS de 1967"
if
__name__
==
"__main__"
:
ma_ds =
CitroenDs
(
)
print
(
ma_ds.marque)
print
(
ma_ds.modele)
print
(
ma_ds.type_suspension)
print
(
ma_ds.statut_moteur
(
))
ma_ds.start_moteur
(
)
print
(
ma_ds.statut_moteur
(
))
Nous faisons ici un héritage multiple. Une voiture Citroën hérite donc à la fois des caractéristiques de la classe voiture et de celle de la classe Citroen, étant les deux à la fois.
IV. Métaclasses▲
Je vous propose de monter encore d'un cran dans les concepts avancés. Python possède un type particulier de classe : les métaclasses.
Pour simplifier, et distinguer une classe d'une métaclasse, il ne faut retenir que la chose suivante : une classe sert à créer un objet, une métaclasse sert à créer une classe.
Ce n'est pas encore très clair ? Rassurez-vous, cela est normal. De prime abord, il est difficile d'appréhender parfaitement ce nouveau concept. Essayons de clarifier tout cela.
IV-A. La métaclasse alpha : type▲
IV-A-1. Au commencement…▲
Considérons les faits suivants :
- en Python, les principaux types de variables sont int, float, str ;
- en Python, tout est objet.
À partir de cela, nous pouvons en déduire que lorsque nous créons des variables, il s'agit d'objets. Cela signifie donc que les types de ces variables (int, float et string) sont donc des classes. Ainsi quand nous écrivons
ma_string =
'Hello World!!!'
Python exécute implicitement
ma_string =
str(
'Hello World!!!'
)
Je pense que vous connaissez bien la fameuse fonction « type », permettant de connaître le type d'une variable donnée. Eh bien au même titre qu'il existe une fonction pour connaître le type d'un objet, il existe une façon de connaître la classe à partir de laquelle un objet a été créé : __class__.
Voici quelques lignes saisies dans un terminal :
>>>
ma_string =
'Hello World'
>>>
mon_entier =
4
>>>
mon_float =
5.3
>>>
ma_string.__class__
<
type 'str'
>
>>>
mon_entier.__class__
<
type 'int'
>
>>>
mon_float.__class__
<
type 'float'
>
>>>
str.__class__
<
type 'type'
>
>>>
int.__class__
<
type 'type'
>
>>>
float.__class__
<
type 'type'
>
>>>
type.__class__
<
type 'type'
>
Vous ne remarquez rien ? Regardez bien. J'ai demandé les classes à partir desquelles les éléments str, int et float étaient créés. Surprise, ils héritent tous d'une classe nommée 'type'. Et si nous demandons la classe permettant la création de la classe type, Python nous renvoie… type.
Eh bien, c'est là que se trouve l'origine de notre langage adoré. 'type' est une métaclasse, LA métaclasse originelle en Python. C'est à partir d'elle que sont créées toutes les classes servant à créer des objets en Python.
Pour finir de vous convaincre, je vous invite à utiliser deux fonctions basiques de Python : 'isinstance' et 'issubclass'.
La première de ces fonctions prend deux paramètres : un objet/classe et une classe. Elle permet de vérifier si l'argument 1 est une instance de l'argument 2 (renvoie True) ou non (renvoie False).
La seconde fonction prend également deux paramètres, plus précisément deux noms de classes disponibles dans l'espace de noms. Elle permet de vérifier si l'argument 1 est une sous‑classe de l'argument 2.
Voici comment vous en servir à travers une utilisation adaptée à notre contexte :
>>>
isinstance(
mon_entier, int)
True
>>>
issubclass(
mon_entier, int)
Traceback (
most recent call last):
File "<stdin>"
, line 1
, in
<
module>
TypeError
: issubclass(
) arg 1
must be a class
>>>
>>>
>>>
isinstance(
int, type)
True
>>>
issubclass(
int,type)
False
Ce qui ressort ici, c'est que « mon_entier » est bien un objet créé à partir de la classe « int », mais n'est pas une classe.
En revanche, « int » est bien une classe (le « issubclass » ne tombe pas en erreur), mais également une instance de « type », c'est-à-dire que « int » a été créé à partir de « type ».
Cela confirme donc bien que « int » est une classe créée à partir de la métaclasse « type ». En effet, « type » est une classe qui sert à créer d'autres classes.
Nous venons donc de vérifier la définition d'une métaclasse.
Vous vous dites peut-être actuellement qu'avoir choisi le mot « type » comme nom à la fois pour une fonction permettant de connaître le type d'un objet, et une classe n'est pas pratique. Eh bien, uniquement à titre indicatif, il s'agit de la même classehttps://docs.python.org/2/library/functions.html?highlight=type#type.
Contrairement à ce que le nom pourrait laisser penser, une métaclasse n'est pas forcément une classe.
Ne soyez donc pas surpris si, lors de la lecture d'un code, la référence métaclasse n'est pas une classe, mais une fonction.
IV-A-2. Son fonctionnement▲
Maintenant que nous avons clairement établi que la classe « type » était une métaclasse, nous allons voir comment elle fonctionne.
Cela est en effet indispensable pour bien appréhender les métaclasses.
« type » prend trois, voire quatre, arguments :
- « cls », dans certains cas, est aux classes ce que « self » est aux objets ;
- le nom de la classe à créer ;
- un tuple contenant les classes dont notre nouvelle classe va hériter ;
- un dictionnaire, lequel contiendra attributs et méthodes de notre classe.
Dans ce dernier, les clés seront les noms des méthodes ou des attributs (dont __init_₎ de la classe. Les valeurs du dictionnaire correspondront aux noms des méthodes ou attributs à utiliser.
Les conventions veulent qu'on nomme les paramètres « name », « bases » et « dct ».
type(
name, bases, dct)
Utiliser ces termes est une fois de plus, comme souvent en Python, juste une forte recommandation. Cependant, respecter cette recommandation vous permettra quelque part de standardiser votre code et d'en faciliter la relecture par un tiers.
IV-A-3. Mise en œuvre▲
La théorie n'étant pas vraiment parlante, surtout dans notre cas, place à la pratique. Je vous propose de continuer avec le cas des voitures, ce type d'objet se prêtant bien aux 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.
33.
34.
35.
36.
37.
38.
39.
40.
# Dans toutes les fonctions, le premier parametre sera toujours
# le nom qui sera passe en premier argument a la classe "type"
def
creation_classe_voiture
(
voiture, marque, modele, couleur):
voiture.marque =
marque
voiture.modele =
modele
voiture.couleur =
couleur
voiture.phares =
False
voiture.moteur =
False
def
commutation_phares
(
voiture):
voiture.phares =
not
voiture.phares
return
voiture.phares
def
commutation_moteur
(
voiture):
voiture.moteur =
not
voiture.moteur
return
voiture.moteur
if
__name__
==
"__main__"
:
# Les classes dont on herite doivent etre placees dans un tuple.
# Si on n'herite d'aucune classe, le tuple sera vide
classes_dont_on_herite =
(
)
# Le dictionnaire de methodes devra obligatoirement contenir
# la methode "__init__" comme nom de constructeur.
mes_methodes =
{ "__init__"
: creation_classe_voiture,
"changement_etat_phares"
: commutation_phares,
"commutation_moteur"
: commutation_moteur}
# On commence par creer notre classe
classe_voiture =
type(
"voiture"
, classes_dont_on_herite, mes_methodes)
# Puis on peut enfin s'en servir
ma_voiture =
classe_voiture
(
"Peugeot"
, "504"
, "noir & chrome"
)
print
ma_voiture.commutation_moteur
(
)
print
ma_voiture.commutation_moteur
(
)
print
ma_voiture.changement_etat_phares
(
)
print
ma_voiture.marque
print
ma_voiture.modele
print
ma_voiture.couleur
Comme le montre ce code, à partir de simples fonctions, et de la métaclasse « type », nous avons pu créer une classe « voiture ».
Mais il n'y a là rien de forcément compliqué maintenant désormais pour vous. Il est en revanche plus compliqué de créer sa propre métaclasse que d'en utiliser une.
IV-B. Les méthodes spéciales▲
Nous allons évoquer ici les méthodes et attributs spéciaux les plus usuels. Je veux parler de ces méthodes/attributs, entourés d'un double underscore (« __ »).
Ces méthodes spéciales sont en effet très utilisées dans les métaclasses, car n'oublions pas que le but d'une métaclasse est d'intercepter la création d'une classe afin de la modifier.
Méthode |
Description |
__init__ |
Il s'agit de la méthode qui est automatiquement appelée à la création d'un objet. Toutefois, avant d'exécuter le code que l'on aura écrit, il appellera avant tout __new__. Il s'agit donc plutôt d'une méthode d'initialisation d'objet que de pure création |
__new__ |
Il s'agit là du véritable constructeur. Son seul rôle est la création de l'objet. Cependant, on peut surcharger cette méthode afin de modifier un objet ou une classe. C'est cette fonctionnalité qui sera utilisée dans les métaclasses |
__del__ |
Méthode appelée implicitement lors de la destruction d'un objet (« del objet »). Vous pouvez surcharger cette méthode et ainsi exécuter du code avant la destruction effective de l'objet (par exemple, logger que l'objet est détruit) |
Attribut |
Description |
__name__ |
classe.__name__ |
__doc__ |
Class.__doc__ ou objet.__doc__ |
__dict__ |
objet.__dict__ |
__module__ |
Permet de connaître le nom du module dans lequel est codée une fonction ou une classe. |
__class__ |
Objet.__class__ permet de connaître à partir de quelle classe l'objet a été créé |
__metaclass__ |
Permet de surcharger la méthode __new__. Son utilisation sera vue en détail dans un des exemples qui suivront |
Pour rappel, vous avez la possibilité de préfixer d'un simple underscore une méthode ou un attribut afin de signaler que ce dernier ne doit pas être accessible depuis l'extérieur de la classe. Il s'agit du principe Python selon lequel « en Python tout est public ». Il ne s'agit cependant que d'une simple convention.
Cependant, vous avez la possibilité de préfixer d'un double underscore une méthode ou un attribut de classe. Python comprendra ainsi qu'il ne doit pas y laisser accès hors de la classe.
IV-C. Exemples pratiques▲
Maintenant que nous avons vu comment utiliser la métaclasse originelle, et l'ensemble des bases indispensables, je vous propose ici de créer nos propres métaclasses.
Autrement dit, nous allons créer des classes, qui permettront de créer d'autres classes, qui elles, permettront de créer des objets. Attention cela va se compliquer un peu.
IV-C-1. Création d'une métaclasse de base▲
L'exemple qui suit est inspiré de celui de Wikipédiahttps://fr.wikipedia.org/wiki/M%C3%A9taclasse#Autre_exemple, dans la mesure où cet exemple est proche de l'utilisation typique qui est faite, en général, des métaclasses : tracer les appels de méthodes.
Voici le code en Python 2.x :
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.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
from
types import
FunctionType
# Definition de notre metaclasse
class
Tracer
(
type):
def
__new__
(
cls, name, bases, dct):
print
(
"name=
%s
"
%(
name))
print
(
"bases=
%s
"
%(
bases))
print
(
"methodes=
%s
"
%(
dct))
def
_wrapper
(
method):
def
_trace
(
self, *
args, **
kwargs):
print
(
"(call
%s
with *
%s
**
%s
)"
%
(
method.__name__
, str(
args), kwargs))
return
method
(
self, *
args, **
kwargs)
# On ne change ni le nom, ni la docstring de la classe
_trace.__name__
=
method.__name__
_trace.__doc__
=
method.__doc__
_trace.__dict__
.update
(
method.__dict__
)
# Notre methode de substitution est prete. On la retourne.
return
_trace
newDct =
{}
for
name, method in
dct.items
(
):
# La ligne suivante permet de savoir si on a affaire a une methode
# ou a un attribut
if
type(
method) is
FunctionType
:
print
(
"nom de la methode=
%s
"
%
(
name))
newDct[name] =
_wrapper
(
method)
else
:
print
(
"nom de l'attribut=
%s
"
%
(
name))
newDct[name] =
method
return
type.__new__
(
cls, name, bases, newDct)
class
ClasseDemo
(
object):
"""
Ceci est ma classe de demo
"""
# On definit une metaclasse qui va etre appelee a la creation de la classe ClasseDemo
# Rien ne change dans le reste de notre classe
__metaclass__
=
Tracer
def
__init__
(
self, default_value):
self.a =
1
self._d =
default_value
self.__z =
6
def
get
(
self, b=
0
):
return
self.a *
b
if
__name__
==
"__main__"
:
# On cree une classe puis on appelle une methode et un attribut
print
(
"########################################################"
)
print
(
"## Creation de la classe ##"
)
print
(
"########################################################"
)
objet =
ClasseDemo
(
5
)
print
(
"########################################################"
)
print
(
"## Utilisation de la classe ##"
)
print
(
"########################################################"
)
print
(
objet.get
(
2
))
# L'appel d'un attribut ne declenche aucun tracage
print
(
objet._d)
Et la même chose en Python 3.x :
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.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
from
types import
FunctionType
# Definition de notre metaclasse
class
Tracer
(
type):
def
__new__
(
cls, name, bases, dct):
print
(
"name=
%s
"
%(
name))
print
(
"bases=
%s
"
%(
bases))
print
(
"methodes=
%s
"
%(
dct))
def
_wrapper
(
method):
def
_trace
(
self, *
args, **
kwargs):
print
(
"(call
%s
with *
%s
**
%s
)"
%
(
method.__name__
, str(
args), kwargs))
return
method
(
self, *
args, **
kwargs)
# On ne change ni le nom, ni la docstring de la classe
_trace.__name__
=
method.__name__
_trace.__doc__
=
method.__doc__
_trace.__dict__
.update
(
method.__dict__
)
# Notre methode de substitution est prete. On la retourne.
return
_trace
newDct =
{}
for
name, method in
dct.items
(
):
# La ligne suivante permet de savoir si on a affaire a une methode
# ou a un attribut
if
type(
method) is
FunctionType
:
print
(
"nom de la methode=
%s
"
%
(
name))
newDct[name] =
_wrapper
(
method)
else
:
print
(
"nom de l'attribut=
%s
"
%
(
name))
newDct[name] =
method
return
type.__new__
(
cls, name, bases, newDct)
class
ClasseDemo
(
object, metaclass =
Tracer):
"""
Ceci est ma classe de demo
"""
# On definit une metaclasse qui va etre appelee a la creation de la classe ClasseDemo
# Rien ne change dans le reste de notre classe
def
__init__
(
self, default_value):
self.a =
1
self._d =
default_value
self.__z =
6
def
get
(
self, b=
0
):
return
self.a *
b
if
__name__
==
"__main__"
:
# On cree une classe puis on appelle une methode et un attribut
print
(
"########################################################"
)
print
(
"## Creation de la classe ##"
)
print
(
"########################################################"
)
objet =
ClasseDemo
(
5
)
print
(
"########################################################"
)
print
(
"## Utilisation de la classe ##"
)
print
(
"########################################################"
)
print
(
objet.get
(
2
))
# L'appel d'un attribut ne declenche aucun tracage
print
(
objet._d)
Comme vous pouvez le constater, l'appel à la métaclasse diffère entre la branche 2 et 3 de Python. Attention donc.
Quelques commentaires concernant ce code.
Ligne 38 ou 44, nous définissons la métaclasse que nous désirons utiliser : Tracer.
Notre classe Tracer, ligne 5, ne contient qu'une unique méthode : __new__. En métaclasse, c'est généralement la seule classe utilisée.
Dans __new__, le premier argument est cls (rappel : équivalent du self pour les classes), puis viennent le nom de la classe, dont elle hérite, et le dictionnaire des méthodes.
Ligne 11, nous définissons une méthode de substitution. Le but final est de ne pas modifier le fonctionnement initial, et d'ajouter un « print » en sus.
Ligne 12, nous retrouvons donc la création de la substitution. Nous interceptons l'ensemble des arguments (nommés ou non) passés en paramètres, puis nous les affichons, avant de retourner le résultat de l'appel de la méthode initiale, ligne 14.
Ligne 16 et 17, nous récupérons le nom et la docstring de la méthode d'origine, puis nous en copions le dictionnaire, ligne 18, afin d'obtenir une copie parfaite.
Ligne 20 enfin, lorsque notre méthode de substitution est prête, nous la renvoyons à l'appelant.
L'appelant dans notre cas, est constitué de la seconde partie du code de notre métaclasse, de la ligne 22 à 33.
En effet, lorsque nous pénétrons dans la méthode __new__ de notre métaclasse, c'est cette section de code qui est exécutée en premier.
Tout d'abord, nous analysons chaque élément du dictionnaire (ligne 23). Puis, lignes 26 et 29, nous déterminons le type d'élément : attribut ou méthode. S'il s'agit d'une méthode, alors, nous appelons notre wrapper, ligne 11. Nous substituons alors notre nouvelle méthode à celle d'origine. Sinon, nous nous contentons de copier l'attribut d'origine.
Afin d'effectuer l'ensemble de ces opérations, un dictionnaire tampon est utilisé.
Enfin, une fois cette analyse et les substitutions effectuées, nous appelons, ligne 33, la métaclasse « type », en lui communiquant non pas le dictionnaire d'origine, mais le dictionnaire tampon. La classe créée est alors retournée.
V. Conclusion▲
Comme nous venons de le voir, l'héritage et les métaclasses constituent des notions avancées, mais font néanmoins partie des indispensables à connaître.
Mais soyons réalistes. Autant les notions d'héritage vous seront utiles très régulièrement, autant les métaclasses, elles, vous serviront extrêmement peu. Il s'agit en effet d'une des notions très avancées Python, utilisée dans des cas complexes et extrêmement spécifiques.
Bien qu'il soit relativement succinct, j'espère que cet article vous aura permis de comprendre l'importance et la puissance de ces concepts, et vous permettra à l'avenir de structurer encore mieux votre code.