Thématiques principales

jeudi 7 mars 2019

Python : Les classes et méta-classes

Un article, court celui ci pour explorer le concept de class dans python. ici rien de compliqué, l’approche est classique comme dans tout autre langage. On déclare un mot clef spécifique un nom, et on par sur une surcharge éventuelle des méthodes de base de la classe Object dont on dérive par défaut.

La classe

Ainsi sera par  exemple amener à surcharger une où plusieurs méthodes __init__ dont le rôle est l’initialisation de votre instance. A noter que cette instance est créée par la méthode __new__ qui elle n’est pas à surcharger (enfin on peut mais c’est pas bien) et qui est en fait le vrai constructeur de notre classe.

On notera au passage que l’on pourra distinguer deux types de méthodes, celles de classe comme __new__ au quelle la classe elle même est passé en paramètre et celle d’instance auxquelles c’est le nouvel objet fraîchement qui est créé qui va etre passé en paramètre.



Prenons un exemple cela sera plus simple:


class Voiture():
    
    nbrInstance=0;
    
    def __init__(self,modele="default", couleur="default"):
        Voiture.nbrInstance+=1
        self.modele=modele
        self.couleur=couleur

v1=Voiture()
v2=Voiture("Alpha","Rouge")
print(v1)
print(v2)

<__main__.Voiture object at 0x03493410>
<__main__.Voiture object at 0x034934D0>
'default'


Dans cet exemple nous voyons donc que nous avons construit une classe voiture, celle ci comporte une donnée membre de classe (le nombre d’instance) et deux données membres d’instances (le modèle et la couleur) auxquelles nous avons associé des valeurs par defaut.

On se rend compte que nous avons malgré tout un soucis avec cette classe car lorsque l’on souhaite l’afficher, celle ci ne donne qu’un affichage sommaire.

Pour remédier à cela, nous allons chercher ce qui initialement nous permet d’avoir cet affichage “par défaut”.

Methodes speciales

Nous allons afficher le contenu des méthodes de la classe objet afin d’en saisir les capacités


print(dir(object))
help(object.__new__)
help(object.__init__)


['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
Help on built-in function __new__:

__new__(*args, **kwargs) method of builtins.type instance
    Create and return a new object.  See help(type) for accurate signature.

Help on wrapper_descriptor:

__init__(self, /, *args, **kwargs)
    Initialize self.  See help(type(self)) for accurate signature.


Nous y découvrons un certain nombre de méthodes, toutes définies et encadrées avec des __. Ce sont les méthodes par défaut de la classe object. Leur surcharge va permettre de préciser certains comportement et entre autre ici, nous allons nous intéresser à __str__ et __repr__ [datamodel-py].

Pourquoi y en a t’ il deux vous allez me dire? et bien simplement parce que leur utilisation n’est pas destiné au même objectif. La première __str__ permet de fournir une chaîne de caractère à la fonction print par exemple alors que la second est associé à la représentation de l’objet dans la console python. Cela semble très subtile comme difference mais bon ne vous inquiétez pas, par défaut, dans les deux cas si l’un est surchargé mais pas l’autre, c’est cette surcharge qui fera fois dans les deux cas…. (à noter qu’il existe un dictionnaire par defaut dans l’objet contenant les propriétés accessible via __dict__)


class Voiture():
    
    nbrInstance=0;
    
    def __init__(self,modele="default", couleur="default"):
        Voiture.nbrInstance+=1
        self.modele=modele
        self.couleur=couleur

    def __repr__(self):
        return "Je suis une Representation de voiture {} {}".format(self.modele,self.couleur)
        
    def __str__(self):
        return "Je suis une voiture {} {}".format(self.modele,self.couleur)
        
        
v1=Voiture()
v2=Voiture("Alpha","Rouge")
print(v2)
v1
print(v.__dict__["modele"])


Je suis une voiture Alpha Rouge
Je suis une Representation de voiture default default


Au détour de ces méthodes on peut noter qu’il existe un bon nombre d’autres méthodes surchargeables. Par exemple, pour permettre de rendre comparable les objets entre eux (__eq__, __lt__, __le__, etc…) ou retourner la taille avec __sizeof__.

Ceci correspond à des méthodes par défaut qu’il est possible de surcharger mais il est aussi possible d’ajouter des traits non initialement présent mais permettant de placer notre objet dans ces conditions d'exécutions particulières.

En fait nous avons déjà vu ça avec les itérateurs de l’article précédent [iterator-py]

En effet, selon les mots clefs employés sur un objet, des opérateurs spécifiques sont attendus.

Par exemple si vous souhaitez utiliser les [] comme avec un vrai dictionnaire sans passer par __dict__ , il vous faudra surcharger (implémenter) les méthodes __getitem__, __setitem__ et __delitem__.


class Voiture():
    
    nbrInstance=0;
    
    def __init__(self,modele="default", couleur="default"):
        Voiture.nbrInstance+=1
        self.modele=modele
        self.couleur=couleur

    def __repr__(self):
        return "Je suis une Representation de voiture {} {}".format(self.modele,self.couleur)

    
    def __getitem__(self,name):
        return self.__getattribute__(name)
    
    def __setitem__(self,name,valeur): 
        return self.__setattr__(name,valeur)
    
    def __delitem__(self):
        return "!"
    
v=Voiture("Alpha","Rouge")

print(v)
print(v["modele"])
v["modele"]="Ciroen"
print(v)


Je suis une Representation de voiture Alpha Rouge
Alpha
Je suis une Representation de voiture Ciroen Rouge


Cool non? Alors il est possible d’aller plus loin en surchargeant par exemple les opérateurs arithmétique __add__, __sub__, __mul__ etc… (attention à gérer la commutativité avec les pendant __radd__, __rsub__, etc...)

L'héritage

Maintenant que nous avons vu les bases avec notre voiture, regardons comme on pourrait créer d’autres type de véhicule.


class Vehicule():
    
    nbrInstance=0;
    
    def __init__(self,nbrRoue,modele="default", couleur="default"):
        Voiture.nbrInstance+=1
        self.nbrRoue=nbrRoue
        self.modele=modele
        self.couleur=couleur

    def __repr__(self):
        return "Je suis un vehicule a {} roues".format(self.nbrRoue)
    
    def roule(self):
        pass

class Voiture(Vehicule):
    
    def __init__(self,nbrRoue=4,modele="default", couleur="default"):
        Vehicule.__init__(self,nbrRoue,modele,couleur)

    def __repr__(self):
        return Vehicule.__repr__(self) +" Je suis une voiture {} {}".format(self.modele,self.couleur)
    
    def roule(self):
        return "Je roule en voiture: "+ self.__repr__()
        
class Moto(Vehicule):
    
    def __init__(self,nbrRoue=2,modele="default", couleur="default"):
        Vehicule.__init__(self,nbrRoue,modele,couleur)
        self.modele=modele
        self.couleur=couleur

    def __repr__(self):
        return Vehicule.__repr__(self) +" Je suis une moto {} {}".format(self.modele,self.couleur)
    
    def roule(self):
        return "Je roule en moto: "+ self.__repr__()
        
l=[Voiture(modele="Alpha",couleur="Rouge"),Moto(modele="Yam",couleur="Bleu")]        
for vehicule in l:
    print(vehicule.roule())


Je roule en voiture: Je suis un vehicule a 4 roues Je suis une voiture Alpha Rouge
Je roule en moto: Je suis un vehicule a 2 roues Je suis une moto Yam Bleu


Dans cet exemple on met en oeuvre une hiérarchie de classe déclinant des type de véhicules avec l’utilisation de valeur par défaut au niveau des constructeurs et surchargeant les méthodes __repr__ et roule afin d’illustrer qu’il est possible de faire du polymorphisme en python.

Il n’y a donc pas forcément beaucoup plus à en dire sur l'héritage en python sauf peut être parler un peu de sa gestion de l'héritage multiple.

Considérons donc maintenant un véhicule d’urgence. Ce nouveau type de véhicule pourrait être un véhicule de type moto mais aussi de type voiture. Comment concilier les deux?

On va créer une nouvelle classe Urgence qui définit la méthode sonne. On va ensuite se créer un nouveau type de véhicule dérivant des Voiture et Moto et implémentant en plus la classe Urgence (oups j’ai un peu utiliser le jargon Java mais en fait ca revient ici au même que d’utiliser une interface).


class Urgence():
    
    def sonne(self):
        return "ouin!! ouin!!"


class VoitureUrgence(Voiture, Urgence):
    
    def __init__(self,nbrRoue=4,modele="default", couleur="default"):
        Voiture.__init__(self,nbrRoue,modele,couleur)
    
    def roule(self):
        return Voiture.roule(self)+Urgence.sonne(self)
        
class MotoUrgence(Moto, Urgence):
    
    def __init__(self,nbrRoue=2,modele="default", couleur="default"):
        Moto.__init__(self,nbrRoue,modele,couleur)
       
    def roule(self):
        return Moto.roule(self)+Urgence.sonne(self)
        
l=[VoitureUrgence(modele="Alpha",couleur="Rouge"),MotoUrgence(modele="Yam",couleur="Bleu")]        
for vehicule in l:
    print(vehicule.roule())      
    
for vehicule in l:
    print(vehicule.sonne())


Je roule en voiture: Je suis un vehicule a 4 roues Je suis une voiture Alpha Rougeouin!! ouin!!
Je roule en moto: Je suis un vehicule a 2 roues Je suis une moto Yam Bleuouin!! ouin!!
ouin!! ouin!!
ouin!! ouin!!


On fera par contre attention sur l'héritage multiple impliquant des classes ayant les mêmes antécédent. En effet il faudra être conscient que par le jeux de la surcharge et du polymorphisme certain comportement attribué à l’un pourrait être réalisé par la définition de l’autre. Dans ces cas il sera alors nécessaire d'être explicite afin de lever les ambiguïtés… Mais le mieux c’est d'éviter…. c’est toujours possible…. (on y arrive bien en Java….)
Un peu meta
On arrive sur la partie la plus passionnante (quand c‘est meta, c’est toujours tres fun), alors on pourrait parler des décorateurs ca sera le propos d’un autre article, du coup je vous propose plutôt de nous lancer dans la réflexion et la meta programmation.

La réflexion


Quoi? Qu’est ce que c’est la réflexion et la meta-programmation? Ok reprenons… tout à l’heure, au début de l’article nous avons constaté que la forme de représentation de nos objets ne nous convenaient pas. Pour cela nous avons cherché comment c’etait gérer au sein de la classe Object et nous avons utiliser la fonction dir pour explorer le contenu de cette classe … voilà, la réflexion, c’est ca. C’est lorsque l’on va chercher à programmatiquement explorer la nature de ce que sont fait les objets afin d'exécuter certaines méthodes de ces derniers sans forcément les connaitres…

Quoi c’est pas très clair? ok prenons un exemple simple: Nous avons des véhicules mais nous souhaitons juste itérer sur leurs propriétés sans forcément nous préoccuper de leur nom. On sait juste que ce sont des propriétés donc que pour les récupérer, on peut passer par la méthode __dict__:


for vehicule in l:
    for (index,prop) in enumerate(vehicule.__dict__):
        print(index,prop,vehicule.__getattribute__(prop))


0 nbrRoue 4
1 modele Alpha
2 couleur Rouge
0 nbrRoue 2
1 modele Yam
2 couleur Bleu


Voila, ceci est de l’introspection, c’est à dire faire l’exploration des objets et de leur intimité sans en connaître à priori leur fonctionnement ou leur contenu.
Après cela ensuite le mieux c’est l’emploi des méta-classes

Les méta-classes

Qu’est ce que sont les méta-classes? Ce sont les classes des classes…. certes dit comme ça ce n’est pas très clair.

Ce qu’il faut comprendre c’est que, comme nous l’avons vu jusqu'à maintenant, nos objets sont construit à partir de classes. On dit qu’ils sont instanciés. Pourtant cette logique est plus complexe car nos propres classes sont elles-même des objets et si celles-ci sont des objets, cela signifie qu’elles ont elles aussi été instanciées à partir d’une classe! C’est cette dernière que l’on appelle alors méta-classe.



Ainsi de la même manière qu’une classe permet de produire un ensemble d’instance du même type (celui de la classe),  une méta-classe permet de produire un ensemble de classes de même type (celui de la méta-classe). Celui qui est un peu familiarisé avec le MDE reconnaîtra rapidement le pattern en escalier de la modélisation. On notera au passage que cette logique de construction n’est pas nouvelle et existe depuis la création du concept objet et de smalltalk.

Quand on se replace dans le contexte de python, alors si nos objets sont issus directement de nos classes, ces dernières étant elles aussi des objets, la différence est que celles-ci sont elles, des instances de la classe “type”.


class MaSuperClass():
    pass

class MaClass(MaSuperClass):
    pass

t=type("MonType", (MaSuperClass,), {})
print("--------------")
print("quel est le type de t?",type(t))
print("quel est le type de Maclass?",type(MaClass))
print("--------------")
print("Affichons t:",t)
print("Affichons MaClass:",MaClass)

i=t()
c=MaClass()
print("--------------")
print("Quel est le type de l'instance i de t?",type(i))
print("Quel est le type de l'instance c de MaClass?",type(c))
print("--------------")
print("Affichons i",i)
print("Affichons c",c)

print("--------------")
print("Quels sont les classes dont t derives?",t.__bases__)  # ce sont les classes desquelles t derives
print("Quels sont les classes dont MaClass derives?",MaClass.__bases__)  # ce sont les classes desquelles t derives
print("--------------")
print("Quel est la meta classe de t?",t.__class__) # ce c'est la metaclasse de t
print("Quel est la meta classe de MaClass?",MaClass.__class__) # ce c'est la metaclasse de t


--------------
quel est le type de t? <class 'type'>
quel est le type de Maclass? <class 'type'>
--------------
Affichons t: <class '__main__.MonType'>
Affichons MaClass: <class '__main__.MaClass'>
--------------
Quel est le type de l'instance i de t? <class '__main__.MonType'>
Quel est le type de l'instance c de MaClass? <class '__main__.MaClass'>
--------------
Affichons i <__main__.MonType object at 0x03317290>
Affichons c <__main__.MaClass object at 0x03317390>
--------------
Quels sont les classes dont t derives? (<class '__main__.MaSuperClass'>,)
Quels sont les classes dont MaClass derives? (<class '__main__.MaSuperClass'>,)
--------------
Quel est la meta classe de t? <class 'type'>
Quel est la meta classe de MaClass? <class 'type'>


Comme on le voit dans l’exemple, pour instancier une classe, on passe par la fonction type à laquelle on passe un nom, un tuple des classes dont notre instance (notre classe n’oublions pas) va hériter et un dictionnaire contenant la liste des propriétés et des méthodes. En parallèle, l’exemple montre la déclaration classique d’une classe et leur utilisations respectives qui en dehors de l’initialisation est totalement similaire.


Dans l’exemple suivant dérivé sur précédent, on va alors chercher à généraliser la construction de classe et aussi de méta-classes par l'utilisation de méta-classes elles même (puisque celles ci sont aussi des objets après tout, elles ont donc forcément les instances de classes)


class MaMetaClass(type):
    pass

t=type("MonMetaType", (type,), {})
print("--------------")
print("quel est le type de t?",type(t))
print("quel est le type de MaMetaClass?",type(MaMetaClass))
print("--------------")
print("Affichons t:",t)
print("Affichons MaMetaClass:",MaMetaClass)

class Primaire:
    pass

class MaClass(Primaire, metaclass=MaMetaClass):
    pass

i=t("MonType", (Primaire,), {})
print("--------------")
print("Le type de i",type(i))
print("Le type de MaClass",type(MaClass))
print("--------------")
print("i ?",i)
print("MaClass ?",MaClass)

print("--------------")
print("Quels sont les classes dont t derives?",t.__bases__)  # ce sont les classes desquelles t derives
print("Quels sont les classes dont MaMetaClass derives?",MaMetaClass.__bases__)  # ce sont les classes desquelles t derives
print("--------------")
print("Quel est la meta classe de t?",t.__class__) # ce c'est la metaclasse de t
print("Quel est la meta classe de MaMetaClass?",MaMetaClass.__class__) # ce c'est la metaclasse de t

print("--------------")
print("Quels sont les classes dont i derives?",i.__bases__)  # ce sont les classes desquelles t derives
print("Quels sont les classes dont MaClass derives?",MaClass.__bases__)  # ce sont les classes desquelles t derives
print("--------------")
print("Quel est la meta classe de i?",i.__class__) # ce c'est la metaclasse de t
print("Quel est la meta classe de MaClass?",MaClass.__class__) # ce c'est la metaclasse de t



--------------
quel est le type de t? <class 'type'>
quel est le type de MaMetaClass? <class 'type'>
--------------
Affichons t: <class '__main__.MonMetaType'>
Affichons MaMetaClass: <class '__main__.MaMetaClass'>
--------------
Le type de i <class '__main__.MonMetaType'>
Le type de MaClass <class '__main__.MaMetaClass'>
--------------
i ? <class '__main__.MonType'>
MaClass ? <class '__main__.MaClass'>
--------------
Quels sont les classes dont t derives? (<class 'type'>,)
Quels sont les classes dont MaMetaClass derives? (<class 'type'>,)
--------------
Quel est la meta classe de t? <class 'type'>
Quel est la meta classe de MaMetaClass? <class 'type'>
--------------
Quels sont les classes dont i derives? (<class '__main__.Primaire'>,)
Quels sont les classes dont MaClass derives? (<class '__main__.Primaire'>,)
--------------
Quel est la meta classe de i? <class '__main__.MonMetaType'>
Quel est la meta classe de MaClass? <class '__main__.MaMetaClass'>


Dans cette exemple, on voit que de la même manière, on va être capable programmatiquement de construire également des méta-classe.



 Ici nous ne somme pas descendu jusqu’au instance de niveau 0 comme dans l’exemple précédent mais finalement il est possible de représenter ces niveaux hiérarchiques par le pattern de méta modélisation suivant:



Du coup vous allez me dire mais à quoi cela peut il servir de créer des classes dynamiquement via les méta-classe?

Cela permettre d’une part une programmation pour adaptative et plus conceptuelle facilitant la réification des concepts d’un métier au sein d’une méta-structure qui nous facilitera la conception et la création des éléments de notre programme. En gros un programme pour faire du programme, c’est un peu comme si vous construisez votre propre langage de programmation dédié à une problématique donnée.

Bon le but ici, n’est pas de digresser de trop sur les concepts MDE qui sont passionnant techniquement et philosophiquement parlant, nous y reviendrons dans un autre article.

Il nous reste encore malgré tout un point non abordé, c’est comment allons nous alimenter nos classes (et/ou méta-classes) avec des méthodes ou des properties?

En fait nous l’avons déjà vu, il suffit d’alimenter un dictionnaire avec un ensemble de référence sur des variables ou des fonctions et de les donner en paramètres au constructeur de la classe type. Ceci faisant, on assemble les morceaux et la classe prend forme.


def maFonction(cls):
    print("ma cls:",cls)

dic={"var":uneProp, "methode":maFonction}

maclass=type("MaClass",(),dic)

instance=maclass()
instance.methode()


ma cls: <__main__.MaClass object at 0x033067D0>


Voilà donc il serait intéressant de faire un exemple un peu plus poussé de la méta-programmation mais ce contenu est déjà assez conséquent et il arrive un temps où il faut conclure! Nous reviendrons de toute façon sur le MDE et sa mise en pratique.

Références

[datamodel-py] https://docs.python.org/3/reference/datamodel.html
[iterator-py] https://un-est-tout-et-tout-est-un.blogspot.com/2019/03/python-listes-et-generateurs.html

Aucun commentaire:

Enregistrer un commentaire