Le guide ultime et définitif sur la programmation orientée objet en Python à l’usage des débutants qui sont rassurés par les textes détaillés qui prennent le temps de tout expliquer. Partie 6.


J’avais autant envie de me taper la rédaction d’un article que d’un handjob par Raiden par temps d’orage, alors je vous met la musique appropriée.

Prérequis :

  • Avoir compris la partie précédente.
  • Avoir un peu de temps devant soi parce que ça va être long et vous allez bouffer des lignes de code par bottes de 12. Vous êtes prévenus.

Méthodes magiques

Python ajoute à la POO quelques goodies, et notamment les méthodes appelées automatiquement. Vous savez, celles qui sont nommées avec deux doubles underscores comme ça : def __methode__(self). On les nomme parfois les “méthodes magiques”.

Vous avez vu __init__, et j’ai fais un article sur __new__. Mais il y en a d’autres. Un paquet d’autres.

__del__, le coquinou

__del__ est sémantiquement l’inverse de __init__, c’est une méthode appelée quand l’objet est détruit.

import time
 
class Action(object):
 
    def __del__(self):
        print "C'est finiiiiiiiiii"
 
 
a = Action()
del a
## C'est finiiiiiiiiii
 
time.sleep(1) # laisse le temps au GC de faire son taff

On l’utilise pour faire des nettoyages une fois qu’un objet n’est plus utile comme fermer les sockets, les fichiers, etc.

Mais il y a un piège.

Le mot clé del en Python ne détruit pas un objet. Il ne fait que détruire la référence. C’est l’interpréteur Python qui compte les références des objets, et quand un objet n’a plus de référence pointant vers lui, il est marqué pour la suppression.

Ensuite, et seulement ensuite, le garbage collector arrive. Ceci n’est pas prédictible. Il peut arriver tout de suite après, ou mille opérations après. Et il supprime tous les objets marqués pour la suppression.

Alors seulement __del__ est appelée.

Ce qui signifie que __del__ peut être appelée beaucoup plus tard que vous le pensez, ou même pas du tout (si le script s’arrête avant). D’où le time.sleep(1) dans le code pour donner de grandes chance à la méthode d’être appelée.

Donner de la gueule à ses objets

Si vous créez un objet, la machine n’a aucune idée de ce que représente l’objet pour vous. Donc si vous lui demandez d’afficher cet objet, Python va afficher ce que l’objet représente pour lui :

class Hic(object):
 
    def __init__(self, titre, auteur):
        self.titre = titre
        self.auteur = auteur
 
morceau = Hic("5eme Symfony", "Beetlejuice")
print morceau
## <Hic object at 0x1d877d0>
print [Hic('5eme Symfony', 'Beetlejuice'), Hic('La flute enchantee', 'Zarma')]
## [<Hic object at 0x1e54610>, <Hic object at 0x1e545d0>]

En gros il vous donne la classe de laquelle l’objet est issu, et son adresse en mémoire.

Mais si vous manipulez beaucoup ces objets dans un shell, vous êtes plus intéressé par le contenu de l’objet, pour rapidement identifier une instance.

Quand vous faites un print,la méthode __str__ pour récupérer la valeur à afficher. C’est le même comportement que d’appeler str() sur un objet :

dico = {'a': 1, 'b': 2}
print dico
## {'a': 1, 'b': 2}
 
str(dico)
## "{'a': 1, 'b': 2}"

On peut donc coder la méthode __str__ pour obtenir ce résultat :

class Hic(object):
 
    def __init__(self, titre, auteur):
        self.titre = titre
        self.auteur = auteur
 
    def __str__(self):
        return "{} de {}".format(self.titre, self.auteur)
 
 
morceau = Hic("5eme Symfony", "Beetlejuice")
str(morceau) # ne marche que dans un terminal
## u'5eme Symfony de Beetlejuice'
print morceau
## 5eme Symfony de Beetlejuice

Une autre méthode intéressante est __repr__. C’est ce que va utiliser Python quand vous entrez un objet dans un shell sans faire print ou quand vous faites print sur une structure de données (list, dico, etc) qui contient l’objet :

class Hic(object):
 
    ...
 
    def __repr__(self):
        return "Hic({}, {})".format(repr(self.titre), repr(self.auteur))
 
print repr(morceau)
## Hic('5eme Symfony', 'Beetlejuice')
print [Hic('5eme Symfony', 'Beetlejuice'), Hic('La flutte enchantee', 'Zarma')]
## [Hic('5eme Symfony', 'Beetlejuice'), Hic('La flutte enchantee', 'Zarma')]

Quand c’est possible (quand c’est pas trop long), __repr__ doit retourner le code qu’il faut saisir pour recréer l’objet. C’est ce que fait Python pour les objets simples :

print repr([u"Père", u"Noël"])
## [u'P\xe8re', u'No\xebl']
[u"Père", u"Noël"]
## [u'P\xe8re', u'No\xebl']
l = [u'P\xe8re', u'No\xebl']
for x in l:
     print x
 
## Père
## Noël

Copier / coller la valeur de retour de __repr__ dans un shell pour un int, une list, un tuple, un set ou un dico permet de recréer ce dico.

Surcharge des opérateurs

En Python, on ne peut pas surcharger les opérateurs comme en C++ par exemple. Mais comme les opérateurs ne font qu’appeler des méthodes magiques, on peut simplement overrider ces méthodes magiques.

Je vous ai montré comment faire ça pour les comparateurs (=, !=, >, etc). Mais on peut le faire aussi pour les opérateurs tels que /, +, etc.

C’est très pratique pour créer de jolis APIs :

class Met(object):
 
    def __init__(self, nom):
        self.nom = nom
 
    def __str__(self):
        return self.nom
 
    def __add__(self, other):
        """
            Override l'opérateur +
        """
        return Met(str(self) + ' et ' + str(other))
 
    def __sub__(self, other):
        """
            Override l'opérateur -
        """
        return Met(str(self) + ' sans ' + str(other))
 
    def __mul__(self, other):
        """
            Override l'opérateur *
        """
        return Met(str(self) + ' avec plein de ' + str(other))
 
    def __div__(self, other):
        """
            Override l'opérateur /
        """
        return Met(str(self) + ' avec très peu de ' + str(other))
 
    def __mod__(self, other):
        """
            Override l'opérateur %
        """
        return Met(str(self) + ' servi dans ' + str(other))
 
    def __pow__(self, other):
        """
            Override l'opérateur **
        """
        return Met(str(self) + ' relevé avec ' + str(other))
 
    def __lshift__(self, other):
        """
            Override l'opérateur <<
        """
        return Met(str(self) + ' après ' + str(other))
 
    def __and__(self, other):
        """
            Override l'opérateur &
        """
        return  Met(str(self) + ' accompagné de ' + str(other))
 
    def __or__(self, other):
        """
            Override l'opérateur |
        """
        return Met(str(self) + ' à la place de ' + str(other))
 
 
 
plat = Met('Canard laqué') + Met('son fond de volaille')
plat -= Met('vinaigrette') * Met('frites') / Met('sel')
plat = plat ** Met('du piment') << Met('une entrée de chorizo')
 
print plat & Met('banana split') | Met('poire belle hélène')
## Canard laqué et son fond de volaille sans vinaigrette avec plein de frites avec très peu de sel relevé avec du piment après une entrée de chorizo accompagné de banana split à la place de poire belle hélène

Peewee utilise cela pour permettre de faire des requêtes très expressives.

Ceci n’est qu’un échantillon des méthodes magiques liées aux opérateurs. La liste complète est ici.

Conversion

class Degre(object):
 
    def __init__(self, valeur, degre='C'):
 
        self.valeur = valeur
        self.degre = degre
 
    def __str__(self):
        return "{} °{}".format(self.valeur, self.degre)
 
 
    def __int__(self):
        """
            Comportement quand converti en entier.
        """
        return int(self.valeur)
 
 
    def __float__(self):
        """
            Comportement quand converti en float.
        """
        return float(self.valeur)
 
 
    def __add__(self, other):
        """
            Pour le fun
        """
        if self.degre != other.degre:
            raise ValueError("Can't add {} and {}".format(self.degre, other.degre))
 
        return Degre(self.valeur + other.valeur, self.degre)
 
    def __index__(self):
        """
            Comportement quand utilisé dans un slicing
        """
        return int(self)

La manipulation des températures se fait facilement :

t1 = Degre(10, "C")
t2 = Degre(3)
print t1 + t2
## 13 °C
print t1 + Degre(10, 'F')
## Traceback (most recent call last):
##   File "<ipython-input-66-bc724ef7a556>", line 1, in <module>
##     t1 + Degre(10, 'F')
##   File "<ipython-input-62-6fa11d434082>", line 32, in __add__
##     raise ValueError("Can't add {} and {}".format(self.degre, other.degre))
## ValueError: Can't add C and F

Et on peut convertir tout ça :

print int(t1)
## 10
print float(t2)
## 3.0

Ce qui est utile dans ce cas là :

print 1 + t1
## Traceback (most recent call last):
##   File "<ipython-input-48-4dc33235a03a>", line 1, in <module>
##     print 1 + t1
## TypeError: unsupported operand type(s) for +: 'int' and 'Degre'
 
print 1 + int(t1)
## 11

On peut même utiliser l’objet dans un contexte inattendu :

douleur = range(10)
print douleur[t2]
## 3

Il y a bien entendu plein d’autres conversion possibles : octal, arrondi, complexe…

Programmation dynamique

Parce que oui, messieurs, on peut faire des trucs dynamiques avec Python. Pas du niveau de Lisp, certes, mais plus qu’en assembleur.

class Tronc(object):
 
 
    def __getattr__(self, name):
        """
            Est appelée quand on demande un attribut appelé "name" et qu'il
            n'existe pas.
        """
        return None
 
    def __setattr__(self, name, value):
        """
            Est appelée quand on assigne une valeur "value" à un attribut 
            appelé "name", qu'il existe ou non.
 
            L'inverse se fait avec __delattr__ (qui réagit à del obj.attr)
        """
        print "Merci"
        super(Tronc, self).__setattr__(name, value)

Ce qui nous permet de réagir sur la manipulation des attributs :

pers = Tronc()
print pers.main # pas d'erreur !
## None
print pers.pied
## None
pers.testicules = "00"
## Merci
print pers.testicules
## 00

Un usage très courant de __getattr__ est de dire que si l’attribut n’existe pas, on retourne l’attribut de la stratégie sous-jacente :

class Parseur(object):
    def __getattr__(self, name):
        return getattr(self.strategy, name)

Relisez les tutos précédents si vous ne vous souvenez plus du pattern strategy.

Attention cependant, intercepter la manipulation des attributs peut facilement se terminer en boucle infinie :

class Tronc(object):
 
    ...
 
    def __setattr__(self, name, value):
        print "Merci"
        setattr(self, name, value)
 
pers = Tronc()
pers.testicules = "00"
##   File "<statement>", line 19, in __setattr__
  File "<statement>", line 19, in __setattr__
  File "<statement>", line 19, in __setattr__
  File "<statement>", line 19, in __setattr__
  ...
  File "<statement>", line 19, in __setattr__
  File "<statement>", line 19, in __setattr__
  File "<statement>", line 19, in __setattr__
  File "<statement>", line 19, in __setattr__
  File "<statement>", line 19, in __setattr__
  File "<statement>", line 19, in __setattr__
  File "<statement>", line 19, in __setattr__
  File "<statement>", line 18, in __setattr__
  File "/opt/reinteract/lib/reinteract/stdout_capture.py", line 27, in write
    self.current.write(str)
  File "/opt/reinteract/lib/reinteract/stdout_capture.py", line 89, in write
    self.__write_function(str)
  File "/opt/reinteract/lib/reinteract/statement.py", line 193, in __stdout_write
    s = self.__coerce_to_unicode(s)
  File "/opt/reinteract/lib/reinteract/statement.py", line 157, in __coerce_to_unicode
    if not isinstance(s, basestring):
RuntimeError: maximum recursion depth exceeded while calling a Python object

En effet pers.testicules = "00" déclenche __setattr__ qui déclenche setattr qui déclenche __setattr__ qui déclenche setattr, etc…

C’est pour cette raison que dans la classe ci-dessus, j’ai fait:

    def __setattr__(self, name, value):
        """
            Est appelée quand on assigne une valeur à un attribut, qu'il existe
            ou non.
 
            L'inverse se fait avec __delattr__ (qui réagit à del obj.attr)
        """
        print "Merci"
        super(Tronc, self).__setattr__(name, value)

On appelle le __setattr__ du parent, qui va assigner l’attribut, mais n’est pas concerné par l’interception.

Ce problème est à garder en tête avec une des méthodes magiques les plus dangereuses qui existe :

__getattribute__(self, name)

Cela fonctionne comme __getattr__, mais pour TOUS les attributs. Même si ils existent. Le potentiel de meli-melo avec cette méthode est de magnitude 7, donc à utiliser à vos risques et périls.

Oui je l’utilise, oui.

Oui je passe 3h en debug à chaque fois que je le fais.

Dans tous les cas, utiliser les properties (faites un flashback…) ou les descripteurs peut être une bonne alternative.

[last_minute_insert]
J’avais oublié __dir__ qui est aussi overridable et qui intercepte dir(objet). Just sayin’…
[/last_minute_insert]

Conteneurs

Parfois, on a besoin d’avoir le comportement d’un conteneur comme un dico ou une liste, mais avec des comportements spécialisés. Il y a des méthodes magiques spécialement pour ça :

class Main(object):
 
 
    def __init__(self, *args):
        self.cartes = args
 
 
    def ajouter(self, carte):
        assert hasattr(carte, upper), "La carte doit etre une string, dude"
        self.cartes.append(carte.upper())
 
 
 
    def __str__(self):
        return u''.join(self.cartes).encode('utf8')
 
 
    def __len__(self):
        """
            Est appelé quand on fait len() sur l'objet.
 
            Utile pour donner une longeur à un objet
        """
        return len(self.cartes)
 
 
    def __getitem__(self, key):
        """
            Est appelé quand on fait objet[index] ou objet[key].
 
            Utile pour simuler une liste ou un dico.
        """
        return self.cartes[key]
 
 
    def __setitem__(self, key, value):
        """
            Est appelé quand on fait objet[index] = "truc"
        """
        self.cartes[key] = value
 
 
    def __delitem__(self, key):
        """
            Est appelé quand on fait del objet[index].
        """
        raise TypeError("Tu ne peux pas m'effacer, mouhahahahaah !")
 
 
    def __iter__(self):
        """
            Est appelé quand on fait un iter(objet), en particulier, cela
            arrive à chaque boucle for.
 
            La valeur retournée doit être un iterateur.
 
            En général on retourne une valeur retournée par iter()
        """
        return iter(self.cartes)
 
 
    def __reversed__(self):
        """
            Est appelé quand on fait reversed(objt)
        """
        return reversed(self.cartes)
 
 
    def __contains__(self, item):
        """
            Est appelé quand "in objet"
        """
        return item in self.cartes
 
 
 
 
main = Main(u'1Coeur', u'7Pique')
print main
## 1Coeur7Pique
 
for carte in main: # parce qu'on a défini __iter__ !
    print carte
## 1Coeur
## 7Pique
 
print main[0] # __getitem__ !
## 1Coeur
 
print 'fdjsklfd' in main # __contains__ !
## False
 
print len(main)
## 2

Bon, ici on aurait presque pu utiliser une liste directement, ou même hériter d’une liste. CEstPourLExemple©

On peut faire aussi des trucs sur les slices avec __getslice__ / __setslice__. C’est le même principe.

Divers, autres, à classer, en vrac…

__enter__ et __exit__, dont j’ai parlé dans l’article sur les context managers.

__format__(self, formatstr)

Appelé quand on fait "{:formater}".format(ta_variable) et pour lequel formatstr contiendra “formater”. Dans le cas où vous vouliez définir un truc qui a plusieurs formats.

__instancecheck__(self, instance) et __subclasscheck__(self, subclass) pour les fans d’instrospection qui veulent intercepter isinstance(objet) et issubclass(objet). Parce qu’on ne sait jamais, des fois qu’on veuille faire un truc super mega vilain à ses collègues qu’ils mettront des mois à debugger après son licenciement.

Plus intéressant :

__call__

Permet de rendre un objet “callable”, c’est à dire appelable comme une fonction.

class Question(object):
 
    def __call__(self, question):
 
        return 'Parce que'
 
q = Question()
q('Pourquoiiiiiiiii ?')
## 'Parce que'

Ça ne sert pas tous les jours, mais ça peut être pratique si votre objet va être placé dans une liste de fonctions.

A noter aussi l’existence de trucs exotiques comme __copy__, __deepcopy__, __getstate__, __setstate__ ou encore __reduce__ qui servent à cloner des objets, les sérialiser, etc. Ce sont des considérations assez avancées, qui mériteraient un article à part entière. Mais c’est aussi marrant que de la compta donc l’attendez pas pour demain.

Bon, vous commencez à avoir une sacré besace d’outils pour faire de la POO.

La prochaine partie, je pense que je vais prendre le code de path.py et le décortiquer sous vos yeux histoire que vous voyez ce qu’on peut faire avec un cas concret d’utilisation de la POO.


Télécharger le code de l’article

20 thoughts on “Le guide ultime et définitif sur la programmation orientée objet en Python à l’usage des débutants qui sont rassurés par les textes détaillés qui prennent le temps de tout expliquer. Partie 6.

  • sil

    Au début de l’article, il manque la coloration syntaxique du code.

  • roro

    Du python en tranches fines…Un régal.
    Et garanti sans cheval. Même les anglais vont venir.
    Ménagez vous, nous tenons beaucoup à vous.
    (plus qu’à la pyfoundationorg, si tu vois ce que je veux dire..%)

  • roro

    Le code téléchargé: C’est la grande classe.
    ça fait du bien de voir de la mécanique aussi bien huilée.
    quand on viens de batailler pour empêcher la p…n de console de se refermer.

  • arthurdent

    Je dois, à ce jour, reconnaitre que VOUS avez plus fait pour ma compréhension de la POO (en 3 jours) que cette putain de formation en 4 semaines.
    Merci Môssieur !
    Je paie en Toblérone alors envoyez l’adresse !!

  • Sam Post author

    On ne peut recevoir que les bitcoins et les dons flattr. Nous invitons tout autre forme de don à être reversé à la Python Software Fondation, ce qui est presque comme nous les donner à nous même.

    Sinon pour la formation, je comprends. Je suis devenu formateur justement parceque je trouvais la plupart des formations très très mauvaise.

  • Glos Flood

    Il me semble que dans l’exemple de la surcharge des opérateurs il manque un underscore pour le mod ;)

    def __mod_(self, other):
  • roro

    A propos de la remarque de Glos Flood, et dont le sujet m’était passé “à côté”, vu que jusqu’à vous découvrir je ne faisais que de l’empirisme au copié/collé.
    Le: ” …celles qui sont nommées avec deux underscores…”
    Aurait dû être: …avec deux “doubles underscores”.
    (Neewbee inside et: E…c…ge de ch’val.)

  • JeromeJ

    (Tiens, je pige pas functools.total_ordering existait déjà depuis 2.7 apparemment, alors pourquoi il est marqué depuis 3.2 dans la doc python3 (aurait-il été retiré entre temps ?))

  • Sam Post author

    @Jerome: super() ne marche qu’avec Python 3. Ceci dit bon signalement de total_ordering, qui marche très en Python 2.7 (car il a été backporté depuis Python 3.2 après son ajout).

  • Réchèr

    Et voici quelques corrections et tentatives d’explications.

    Reiden
    Raiden

    le garbage collecteur
    le garbage collector
    (quitte à faire un anglicisme, autant le faire jusqu’au bout).

    time.sleep(1)

    Si j’ai bien tout compris, le sleep dans le code, c’est pour donner un peu de temps au garbage collector, afin d’être sûr qu’il se déclenche. Est-ce qu’il ne faudrait pas le dire ? Les explications qui viennent juste après le code ne mentionnent pas du tout le sleep, donc on ne comprends pas forcément ce qu’il vient foutre là.

    Python essaye d’appeler les méthodes __unicode__ et __str__ (je ne sais plus dans quel ordre) pour récupérer la valeur à afficher.

    Pas en python 2.x, en tout cas.
    En 2.x, le print appelle systématiquement __str__, et balance le résultat sur stdout. __unicode__est utilisé uniquement quand on appelle explicitement unicode().

    Python 2.7.2 (default, Jun 12 2011, 15:08:59)
    [MSC v.1500 32 bit (Intel)] on win32
    Type "copyright", "credits" or "license()" for more information.
     
    class CC(object):
        def __unicode__(self):
            return unicode("ceci est de l'unicode")
        def __str__(self):
            return str("ceci est une string")
     
    c = CC()
    c
    # 
    print c
    # ceci est une string
    unicode(c)
    # u"ceci est de l'unicode"
    class DD(object):
        def __unicode__(self):
            return unicode("ceci est de l'unicode DD")
     
    d = DD()
    d
    # 
    print d
    # 
    unicode(d)
    # u"ceci est de l'unicode DD"

    En 3.x, je ne sais pas comment ça marche. Je dirais que le print appelle __str__, puis il essaye de convertir le résultat obtenu (qui est de l’unicode) dans l’encodage de sys.stdout, et qu’il ressort ça comme il peut.

  • Réchèr

    Putaaaaain de commentaires WordPress !!

    Dans le bloc de code du commentaire précédent, remplacer les “#” qui sont tout seul sur une ligne par le gloubiboulga correspondant.

    Pour le premier dièse :

    # __main__.CC object at 0x01DA34B0

    Pour les deux dièses suivants :

    # __main__.DD object at 0x01DA3150
  • Sam Post author

    Pour time.sleep, c’est tout simplement que __del__ peut ne pas être appelé du tout si la VM se ferme avant le passage du GC. Donc je met un slip pour éviter les tâches.

    Pour l’unicode, c’est une connerie de ma part, je retire.

  • sensini42

    On prend les mêmes…

    beaucoup plus tard que vous le pensez : que ce que vous pensez voire, que vous ne pensez

    J’ai mis un moment à voir la blague de la classe Hic…

    faites un print,la : print, la (+espace)

    les . après etc.

    print douleur[t2]
    ## 3

    Le ressenti de la douleur pour comprendre l’exemple est plus important

    Parce que oui, messieurs, on peut faire des trucs dynamiques avec Python : et les dames ? et les 3e sexe ?

    Même si ils existent : s’ils
    de meli-melo : de méli-mélo. Bien que je préférasse d’emméli-mélo

    “La carte doit etre…dude” : doit être

    Est appelé quand on fait… : plus haut, c’était : appelée

    lequel formatstr : balise code

    A noter : À noter

    une sacré besace : sacrée

    Pas si long que ça l’article. Toujours très intéressant.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.