magic methods – Sam & Max http://sametmax.com Du code, du cul Wed, 23 Dec 2020 13:35:02 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 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. http://sametmax.com/le-guide-ultime-et-definitif-sur-la-programmation-orientee-objet-en-python-a-lusage-des-debutants-qui-sont-rassures-par-les-textes-detailles-qui-prennent-le-temps-de-tout-expliquer-partie-6/ http://sametmax.com/le-guide-ultime-et-definitif-sur-la-programmation-orientee-objet-en-python-a-lusage-des-debutants-qui-sont-rassures-par-les-textes-detailles-qui-prennent-le-temps-de-tout-expliquer-partie-6/#comments Wed, 27 Feb 2013 08:14:24 +0000 http://sametmax.com/?p=5136 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
## 
print [Hic('5eme Symfony', 'Beetlejuice'), Hic('La flute enchantee', 'Zarma')]
## [, ]

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 "", line 1, in 
##     t1 + Degre(10, 'F')
##   File "", 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 "", line 1, in 
##     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 "", line 19, in __setattr__
  File "", line 19, in __setattr__
  File "", line 19, in __setattr__
  File "", line 19, in __setattr__
  ...
  File "", line 19, in __setattr__
  File "", line 19, in __setattr__
  File "", line 19, in __setattr__
  File "", line 19, in __setattr__
  File "", line 19, in __setattr__
  File "", line 19, in __setattr__
  File "", line 19, in __setattr__
  File "", 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

]]>
http://sametmax.com/le-guide-ultime-et-definitif-sur-la-programmation-orientee-objet-en-python-a-lusage-des-debutants-qui-sont-rassures-par-les-textes-detailles-qui-prennent-le-temps-de-tout-expliquer-partie-6/feed/ 20 5136