PROJET AUTOBLOG


Sam et Max

source: Sam et Max

⇐ retour index

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.

mercredi 27 février 2013 à 09:14

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

Prérequis :

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)

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 collecteur 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).

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, Python essaye d’appeler les méthodes __unicode__ et __str__ (je ne sais plus dans quel ordre) pour récupérer la valeur à afficher. C’est le même comportement que d’appeler unicode() ou str() sur un objet :

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

On peut donc coder les méthodes __unicode__ et __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)
 
    def __unicode__(self):
        return u"{} de {}".format(self.titre, self.auteur)
 
 
morceau = Hic("5eme Symfony", "Beetlejuice")
str(morceau) # ne marche que dans un terminal
## '5eme Symfony de Beetlejuice'
unicode(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 __unicode__(self):
        return u''.join(self.cartes)
 
    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

flattr this!

Exemples de bons codes Python

mardi 26 février 2013 à 10:47

Yeah, on a des fannnnnnns !

Ça fait quelques semaines que je me suis mis à python. J’ai commencé par des scripts (tendance sysadmin oblige) puis je me suis lancé dans des choses un (petit) peu plus importantes, notamment influencé par les cours sur la POO. Je tiens d’ailleurs à vous féliciter sur ce point, même le site du zéro n’avait jamais réussi à me les faire vraiment comprendre. Et donc, comme tout débutant qui se respecte j’essaie de faire de mon petit programme un chef d’œuvre de perfection (et il y a du boulot).

Le truc, c’est que je ne connais rien aux bonnes pratiques en python (comment commenter utilement les fonctions, les conventions de nommage, les jolies façons de coder, etc…). Je suis à la recherche d’exemples sûrs.

Connaissez-vous des librairies ou applications au code exemplaire dont je pourrais m’inspirer tant au niveau du code lui-même que de l’API ou de la doc ?

(Allez, vous devez bien en avoir à l’esprit ^^)

Bref, merci (si c’est pas pour ça c’est pour le reste) et bonne soirée !

Cher [censored],

Être placé au dessus du site du zéro provoque chez moi une érection incontrôlée. Merci.

Bonne nouvelle, il n’existe rien de tel que le code parfait, et des tas d’excellents dev font de la merde quotidiennement, ce qui permet de relativiser face à son niveau.

Maintenant, si je devais donner des exemples de code et doc dont on peut s’inspirer, je dirais :

Petit rappel ceci dit : même un bon code a toujours des lacunes. Si tu prends n’importe quel bout de ces libs et le mets sur un forum, tu auras toujours un râleur pour venir débattre sur ce qu’il aurait valu mieux faire.

De plus, d’une manière générale, les gens qui publient de bons code font des trucs un peu plus compliqués qu’un simple script. Donc tu auras toujours à froncer un peu les sourcils pour comprendre. C’est normal, ne perd pas courage.

A éviter niveau code : django, bottle, twisted et les frameworks web en général. Les gros projets avec des bindings C de types sqlalchemy, qt, wx , etc. Car là on rentre dans un autre monde.

flattr this!

Téléchargez le blog en entier pour le lire hors ligne

lundi 25 février 2013 à 13:55

Voilà, c’est dumpé, avec un simple :

wget -m -k -p -c -E http://sametmax.com

(d’ailleurs si vous voulez faire votre propre miroir du site vous pouvez faire pareil. Hier apparemment y a eu pas mal d’essais car nos stats ont bien monté pour un dimanche)

J’ai mis tout ça sur Github, mais si vous aimez pas Git vous pouvez télécharger le zip.

Je n’ai pas mis de process automatique pour le faire, donc on refera un dump quand on y pensera.

On a mis tout ça sous Creative Common Unported, ce qui signifie que vous pouvez le réutiliser pour un cours, dans un bouquin et même le réadapter à votre sauce, le traduire en Klingon et le publier en morse.

Y a un paquet de trucs mine de rien, et pas seulement nos articles mais aussi de très très bons commentaires et des posts invités.

J’en profite donc pour remercier :

Du coup on a pas mal de choses sur le repo git maintenant:

Et bien sûr le tout nouveau repo du code des articles qui est un peu vide pour le moment. Donc ma question la suivante : est-ce que vous préférez qu’on remplisse le repo au fur et à mesure des nouveaux articles, ou alors que régulièrement on dépoussière un ancien article et qu’on le mette au nouveau format ?

Sachant que mine de rien des anciens articles sur Python et Django, on commence à en avoir une chiée.

flattr this!

Quelques changements sur le blog

dimanche 24 février 2013 à 12:16

Suite à une demande d’un lecteur acharné, les tutos Python vont changer de format.

Si le code est un peu long, il sera maintenant noté dans la mesure du possible sous la forme:

expression
## resultat

et non plus

>>> expression
resultat

De manière à ce que ce soit facile à copier / coller pour jouer avec.

Et l’intégralité du code du tuto sera mis sur ce repo git.

Vous pouvez déjà voir le résultat de ces changements sur le dernier tuto.

Au passage, vous avez du remarqué que pour les gros tutos de temps en temps on met des videos de musique. C’est aléatoire, la musique n’a pas de thème précis, c’est selon l’humeur.

La plantage de vendredi m’a aussi fait prendre conscience que ce blog ne durera pas toujours, et que quand on arrêtera d’écrire dedans, se serait bête de perdre tout son contenu. Je voudrais donc faire un dump statique du site à mettre en téléchargement à dispo.

Pour le moment j’ai essayé htttrack, mais il a un million d’options et avait pas finit après 3 heures. Puis divers plugins wordpress qui marchent tous moins bien les uns que les autres. Le dump SQL demanderait trop de nettoyage manuel. Le dump XML pareil, il contient les mails des posteurs de comment, et je ne veux pas divulguer vos adresses à qui que ce soit.

Bref, si vous avez une suggestion pour une “one command => mirror site”, faites pêter.

flattr this!

Pourquoi ça a tranché, chérie

samedi 23 février 2013 à 08:02

Juste un petit mot en passant pour expliquer le couac d’hier (et aussi parcequ’on se casse au ski donc les articles attendront) : notre hébergeur, leaseweb (et pas online.net qui était l’ancien hébergeur du blog), a eu quelques problèmes sur son réseau.

Comme ce sont des machines virtuelles, le problème s’est manifesté à notre niveau sous la forme d’un iowait de plus de 90% à la cause impossible à diagnostiquer et qui impliquait de fortes lenteurs sur le blog (et tous nos autres sites) jusqu’à ce qu’on force un reboot (juste sur le blog :-p).

C’était une mauvaise idée, puisque le serveur est devenu injoignable (alors que les autres tournaient mal, mais tournaient).

Au final, leaseweb à bien réagi en communiquant rapidement sur le problème et en remettant le truc d’équerre rapidement.

Ça nous a permis de réaliser que bien qu’on ait un backup du blog, on avait pas de backup de 0bin. C’est maintenant chose faite.

Comme on critique beaucoup Twitter en ce moment, au moins un point positif c’est que quand on se pête la gueule, on peut continuer à communiquer par ce genre de services tiers. Le could-social-over-hyper-3.0 n’a pas que du mauvais.

@+ dans le bus

flattr this!