Concepts avancés Python

Héritage simple, héritage multiple et métaclasses

GALODE Alexandre

Vous êtes-vous parfois fait la réflexion, en lisant le code d'une classe, que vous y auriez ajouté des attributs ? Et que, plutôt que de réécrire ou « forker » cette classe, il serait plus pratique d'écrire un code qui ajouterait une surcouche à cette classe, mais sans savoir comment faire ?

Si tel est le cas, vous êtes tombé sur le bon article.

Commentez

- Titre : Concepts avancés Python

- Auteur : GALODE Alexandre

- Parution : 10 octobre 2015

- Public visé :

- Licence : Le contenu de cet article est rédigé par GALODE Alexandre et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Introduction

- II. Prérequis

- III. L'héritage

- III-A. Principe

- III-B. Héritage simple

- III-B-1. Déclaration de l'héritage

- III-B-2. Initialiser la classe mère

- III-B-3. Surcharger un attribut

- III-B-4. Surcharger une fonction

- III-C. Héritage multiple

- III-D. Le MRO

- III-D-1. Théorie

- III-D-2. Connaître l'ordre d'héritage

- III-D-3. Exemple

- III-E. Exemple pratique

- IV. Métaclasses

- IV-A. La métaclasse alpha : type

- IV-A-1. Au commencement…

- IV-A-2. Son fonctionnement

- IV-A-3. Mise en œuvre

- IV-B. Les méthodes spéciales

- IV-C. Exemples pratiques

- IV-C-1. Création d'une métaclasse de base

- V. Conclusion

- VI. Remerciement

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

Utilisation de l'ancien algorithme

1. class A():

2. pass

4. class B():

5. pass

7. class C(A, B):

8. pass

Utilisation du nouvel algorithme

1. class A(object):

2. pass

4. class B(object):

5. pass

7. class C(A, B):

8. 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__ ».

1. class A(object):

2. pass

4. class B(object):

5. pass

7. class C(A, B):

8. pass

10. if __name__ == '__main__':

11. # Attention, __mro__ est un attribut special de classe.

12. # Il doit donc etre recupere depuis la classe

13. print(C.__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.

1. class A(object):

2. pass

4. class B(object):

5. pass

7. class C(A, B):

8. pass

10. class D(B,A):

11. pass

13. class E(D,A):

14. pass

16. try:

17. class F(A,D):

18. pass

19. except Exception as e:

20. print("Voici l'erreur a la creation de la classe F")

21. print(e)

23. if __name__ == '__main__':

24. print("\nVoici l'execution du main")

25. print(C.__mro__)

26. print(D.__mro__)

27. print(E.__mro__)

Je vous invite à exécuter ce code. Le résultat est alors le suivant :

1. Voici l'erreur a la creation de la classe F

2. Error when calling the metaclass bases

3. Cannot create a consistent method resolution order (MRO) for bases D, A

5. Voici l'execution du main

6. (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <type 'object'>)

7. (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.A'>, <type 'object'>)

8. (<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://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.

voiture.py

1. class Voiture():

2. def __init__(self):

3. self.nombre_roues = 4

4. self.nombre_fauteuils = 1

5. self.moteur = False

6. self.volant = True

8. def start_moteur(self):

9. self.moteur = True

10. return self.moteur

12. def stop_moteur(self):

13. self.moteur = False

14. return self.moteur

16. def statut_moteur(self):

17. return self.moteur

19. if __name__ == "__main__":

20. ma_voiture_basique = Voiture()

21. print(ma_voiture_basique.statut_moteur())

22. ma_voiture_basique.start_moteur()

23. 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.

citroen.py

1. class Citroen():

2. def __init__(self):

3. self.type_suspension = "Hydractives"

4. self.logo = "Chevrons"

5. self.marque = "Citroen"

7. if __name__ == "__main__":

8. ma_citroen = Citroen()

9. print(ma_citroen.type_suspension)

10. 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.

bond_bug.py

1. from voiture import Voiture

3. class BondBug(Voiture):

4. def __init__(self):

5. Voiture.__init__(self)

6. self.marque = "Bond"

7. self.modele = "Bug"

8. self.nombre_roues = 3

9. self.nombre_fauteuils = 2

10. print(self.statut_moteur())

12. def start_moteur(self):

13. # Sur la ligne suivante de code, si nous ne stipulons pas expressement qu'on adresse

14. # l'attribut de Voiture, Python comprendra qu'on fait reference a BondBug.start_moteur

15. # a cause de la surcharge. Le self est la car nous appelons l'objet concerne de la classe

16. # mere

17. state = Voiture.start_moteur(self)

18. print("Statut du moteur: %s" % (state))

20. def stop_moteur(self):

21. state = Voiture.stop_moteur(self)

22. print("Statut du moteur: %s" % (state))

24. if __name__ == "__main__":

25. my_bug = BondBug()

26. my_bug.start_moteur()

27. 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.

citroen_DS.py

1. from voiture import Voiture

2. from citroen import Citroen

4. class CitroenDs(Voiture, Citroen):

5. def __init__(self):

6. Voiture.__init__(self)

7. Citroen.__init__(self)

8. self.modele = "DS de 1967"

10. if __name__ == "__main__":

11. ma_ds = CitroenDs()

12. print(ma_ds.marque)

13. print(ma_ds.modele)

14. print(ma_ds.type_suspension)

15. print(ma_ds.statut_moteur())

16. ma_ds.start_moteur()

17. 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 classe.

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.

1. # Dans toutes les fonctions, le premier parametre sera toujours

2. # le nom qui sera passe en premier argument a la classe "type"

3. def creation_classe_voiture(voiture, marque, modele, couleur):

4. voiture.marque = marque

5. voiture.modele = modele

6. voiture.couleur = couleur

7. voiture.phares = False

8. voiture.moteur = False

10. def commutation_phares(voiture):

11. voiture.phares = not voiture.phares

12. return voiture.phares

14. def commutation_moteur(voiture):

15. voiture.moteur = not voiture.moteur

16. return voiture.moteur

19. if __name__ == "__main__":

20. # Les classes dont on herite doivent etre placees dans un tuple.

21. # Si on n'herite d'aucune classe, le tuple sera vide

22. classes_dont_on_herite = ()

24. # Le dictionnaire de methodes devra obligatoirement contenir

25. # la methode "__init__" comme nom de constructeur.

26. mes_methodes = { "__init__": creation_classe_voiture,

27. "changement_etat_phares": commutation_phares,

28. "commutation_moteur": commutation_moteur}

30. # On commence par creer notre classe

31. classe_voiture = type("voiture", classes_dont_on_herite, mes_methodes)

33. # Puis on peut enfin s'en servir

34. ma_voiture = classe_voiture("Peugeot", "504", "noir & chrome")

35. print ma_voiture.commutation_moteur()

36. print ma_voiture.commutation_moteur()

37. print ma_voiture.changement_etat_phares()

38. print ma_voiture.marque

39. print ma_voiture.modele

40. 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__
renvoie le nom de la classe

__doc__

Class.__doc__ ou objet.__doc__
Renvoie la docstring de la classe interrogée

__dict__

objet.__dict__
Renvoie un dictionnaire des méthodes et attributs disponibles pour l'objet

__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édia, 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 :

Python 2.x

1. from types import FunctionType

4. # Definition de notre metaclasse

5. class Tracer(type):

6. def __new__(cls, name, bases, dct):

7. print("name= %s" %(name))

8. print("bases= %s" %(bases))

9. print("methodes= %s" %(dct))

11. def _wrapper(method):

12. def _trace(self, *args, **kwargs):

13. print("(call %s with *%s **%s)" % (method.__name__, str(args), kwargs))

14. return method(self, *args, **kwargs)

15. # On ne change ni le nom, ni la docstring de la classe

16. _trace.__name__ = method.__name__

17. _trace.__doc__  = method.__doc__

18. _trace.__dict__.update(method.__dict__)

19. # Notre methode de substitution est prete. On la retourne.

20. return _trace

22. newDct = {}

23. for name, method in dct.items():

24. # La ligne suivante permet de savoir si on a affaire a une methode

25. # ou a un attribut

26. if type(method) is FunctionType:

27. print("nom de la methode= %s" % (name))

28. newDct[name] = _wrapper(method)

29. else:

30. print("nom de l'attribut= %s" % (name))

31. newDct[name] = method

33. return type.__new__(cls, name, bases, newDct)

38. class ClasseDemo(object):

39. """

40. Ceci est ma classe de demo

41. """

42. # On definit une metaclasse qui va etre appelee a la creation de la classe ClasseDemo

43. # Rien ne change dans le reste de notre classe

44. __metaclass__ = Tracer

46. def __init__(self, default_value):

47. self.a = 1

48. self._d = default_value

49. self.__z = 6

51. def get(self, b=0):

52. return self.a * b

57. if __name__ == "__main__":

58. # On cree une classe puis on appelle une methode et un attribut

59. print("########################################################")

60. print("##               Creation de la classe                ##")

61. print("########################################################")

62. objet = ClasseDemo(5)

63. print("########################################################")

64. print("##              Utilisation de la classe              ##")

65. print("########################################################")

66. print(objet.get(2))

67. # L'appel d'un attribut ne declenche aucun tracage

68. print(objet._d)

Et la même chose en Python 3.x :

Python 3.X

1. from types import FunctionType

4. # Definition de notre metaclasse

5. class Tracer(type):

6. def __new__(cls, name, bases, dct):

7. print("name= %s" %(name))

8. print("bases= %s" %(bases))

9. print("methodes= %s" %(dct))

11. def _wrapper(method):

12. def _trace(self, *args, **kwargs):

13. print("(call %s with *%s **%s)" % (method.__name__, str(args), kwargs))

14. return method(self, *args, **kwargs)

15. # On ne change ni le nom, ni la docstring de la classe

16. _trace.__name__ = method.__name__

17. _trace.__doc__  = method.__doc__

18. _trace.__dict__.update(method.__dict__)

19. # Notre methode de substitution est prete. On la retourne.

20. return _trace

22. newDct = {}

23. for name, method in dct.items():

24. # La ligne suivante permet de savoir si on a affaire a une methode

25. # ou a un attribut

26. if type(method) is FunctionType:

27. print("nom de la methode= %s" % (name))

28. newDct[name] = _wrapper(method)

29. else:

30. print("nom de l'attribut= %s" % (name))

31. newDct[name] = method

33. return type.__new__(cls, name, bases, newDct)

38. class ClasseDemo(object, metaclass = Tracer):

39. """

40. Ceci est ma classe de demo

41. """

42. # On definit une metaclasse qui va etre appelee a la creation de la classe ClasseDemo

43. # Rien ne change dans le reste de notre classe

45. def __init__(self, default_value):

46. self.a = 1

47. self._d = default_value

48. self.__z = 6

50. def get(self, b=0):

51. return self.a * b

56. if __name__ == "__main__":

57. # On cree une classe puis on appelle une methode et un attribut

58. print("########################################################")

59. print("##               Creation de la classe                ##")

60. print("########################################################")

61. objet = ClasseDemo(5)

62. print("########################################################")

63. print("##              Utilisation de la classe              ##")

64. print("########################################################")

65. print(objet.get(2))

66. # L'appel d'un attribut ne declenche aucun tracage

67. 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.

VI - Remerciement

- Lolo78

- claudeLELOUP