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

dimanche 3 février 2013 à 12:04

Un peu de zik, puisque c’est ce qu’on fait maintenant :

Prérequis :

Aujourd’hui nous allons voir ce qui fait toute la puissance de la programmation orientée objet : l’héritage.

Héritage simple

L’héritage est un moyen de factoriser du code, c’est à dire que si on a le même code à deux endroits, l’héritage permet de centraliser ce code à un seul endroit. Ce n’est pas le seul moyen de faire ça. On peut très bien factoriser du code uniquement avec des fonctions. Néanmoins l’héritage a des caractéristiques qui rendent la factorisation très efficace.

Supposons que vous fassiez un jeu vidéo pour apprendre les prières chrétiennes, et que vous ayez une classe par prière :

class AveMaria:
 
 
    texte = ("Je vous salue, Marie pleine de grâce ;",
            "Le Seigneur est avec vous.",
            "Vous êtes bénie entre toutes les femmes",
            "Et Jésus, le fruit de vos entrailles, est béni.",
            "Sainte Marie, Mère de Dieu,",
            "Priez pour nous, pauvres pécheurs,",
            "Maintenant, et à l'heure de notre mort.",
            "Amen.")
 
 
    def prier(self, nombre_de_fois=1):
 
        for x in xrange(nombre_de_fois):
            for ligne in self.texte:
                print ligne
 
 
 
class PaterNoster:
 
 
    texte = ("Notre Père qui es aux cieux",
            "que ton nom soit sanctifié",
            "que ton règne vienne,",
            "que ta volonté soit faite",
            "sur la terre comme au ciel.",
            "Donne-nous aujourd’hui",
            "notre pain de ce jour,",
            "pardonne-nous nos offenses",
            "comme nous pardonnons aussi",
            "à ceux qui nous ont offensés",
            "et ne nous soumets pas à la tentation",
            "mais délivre-nous du mal.",
            "Amen")
 
 
    def prier(self, nombre_de_fois=1):
 
        for x in xrange(nombre_de_fois):
            for ligne in self.texte:
                print ligne
 
 
>>> piere = AveMaria()
>>> piere.prier()
Je vous salue, Marie pleine de grâce ;
Le Seigneur est avec vous.
Vous êtes bénie entre toutes les femmes
Et Jésus, le fruit de vos entrailles, est béni.
Sainte Marie, Mère de Dieu,
Priez pour nous, pauvres pécheurs,
Maintenant, et à l'heure de notre mort.
Amen.

Ici la méthode prier() est dupliquée. Or, c’est exactement la même. Il n’y a pas de raison de l’écrire deux fois. Nous allons utiliser l’héritage pour centraliser ce code en créant une classe Priere qui contiendra le code commun à toutes les prières :

class Priere:
 
    def prier(self, nombre_de_fois=1):
 
        for x in xrange(nombre_de_fois):
            for ligne in self.texte:
                print ligne

Ensuite, nous allons demander à toutes les classes prières d’hériter de cette classe commune :

class AveMaria(Priere): # <---- cette syntaxe veut dire 'hérite de Priere'
 
    texte = ("Je vous salue, Marie pleine de grâce ;",
            "Le Seigneur est avec vous.",
            "Vous êtes bénie entre toutes les femmes",
            "Et Jésus, le fruit de vos entrailles, est béni.",
            "Sainte Marie, Mère de Dieu,",
            "Priez pour nous, pauvres pécheurs,",
            "Maintenant, et à l'heure de notre mort.",
            "Amen.")
 
 
class PaterNoster(Priere):
 
    texte = ("Notre Père qui es aux cieux",
            "que ton nom soit sanctifié",
            "que ton règne vienne,",
            "que ta volonté soit faite",
            "sur la terre comme au ciel.",
            "Donne-nous aujourd’hui",
            "notre pain de ce jour,",
            "pardonne-nous nos offenses",
            "comme nous pardonnons aussi",
            "à ceux qui nous ont offensés",
            "et ne nous soumets pas à la tentation",
            "mais délivre-nous du mal.",
            "Amen")

Ici, les classes AveMaria et PaterNoster héritent de la classe Priere. On dit qu’elles sont les enfants (ou filles) de Priere. Ou que Priere est la classe parente de AveMaria et PaterNoster

Notez qu’on a retiré la méthode prier() de PaterNoster et AveMaria. Malgré cela, ça marche :

>>> PaterNoster().prier()
Notre Père qui es aux cieux
que ton nom soit sanctifié
que ton règne vienne,
que ta volonté soit faite
sur la terre comme au ciel.
Donne-nous aujourd'hui
notre pain de ce jour,
pardonne-nous nos offenses
comme nous pardonnons aussi
à ceux qui nous ont offensés
et ne nous soumets pas à la tentation
mais délivre-nous du mal.
Amen

Cela marche car quand on hérite d’une classe, le code de cette classe est copié de la classe parente vers la classe enfant. Ainsi, prier() est automatiquement copiée de Priere vers AveMaria et PaterNostre.

Cela marche pour toutes les méthodes, même celles appelées automatiquement. C’est particulièrement utile avec la méthode __init__:

class Priere:
 
    def __init__(self, expiation=False):
        self.expiation = expiation
 
    def prier(self, nombre_de_fois=1):
 
        for x in xrange(nombre_de_fois):
            for ligne in self.texte:
                if self.expiation:
                    print ligne.upper()
                else:
                    print ligne

On rajoute ici un attribut, et il se retrouve dans la classe enfant (si vous êtes dans un shell, n’oubliez pas de réécrire aussi la classe AveMaria à chaque fois, même si elle ne change pas):

>>> priere = AveMaria(expiation=True)
>>> priere.expiation
True
>>> priere.prier()
JE VOUS SALUE, MARIE PLEINE DE GRâCE ;
LE SEIGNEUR EST AVEC VOUS.
VOUS êTES BéNIE ENTRE TOUTES LES FEMMES
ET JéSUS, LE FRUIT DE VOS ENTRAILLES, EST BéNI.
SAINTE MARIE, MèRE DE DIEU,
PRIEZ POUR NOUS, PAUVRES PéCHEURS,
MAINTENANT, ET à L'HEURE DE NOTRE MORT.
AMEN.

L’héritage, c’est juste cela. Pas la peine de chercher une truc compliqué : le code du parent se retrouve dans celui de l’enfant. Cela marche pour les méthodes et les attributs.

Petite appartée sur object

Dans la partie précédente et dans de nombreux codes, vous avez dû voir des classes comme cela :

class MaClass(object): # wtf is this object stuff ?
 
    # code

Il s’agit bel et bien d’héritage, object étant un type buil-in en Python au même titre que les string ou les int :

>>> object
<type 'object'>

La raison de cet héritage bizarre est qu’à partir de Python 2.2, une nouvelle architecture des classes a été introduite qui corrige les problèmes de l’ancienne. Mais pour garder le code compatible, ces nouvelles classes n’ont pas été activées par défaut.

Ainsi, même si vous êtes en Python 2.7, quand vous faites :

class MaClass:
 
    # code

Vous utilisez l’ancien type de classe (old-style class). Pour dire à Python que vous voulez utiliser le nouveau type de classe (new-style class), il faut hériter d’object :

class MaClass(object):
 
    # code

Il n’y a AUCUN intérêt à utiliser les old-style classes. Je l’ai fais en ce début de cours pour éviter d’introduire la notion d’héritage trop tôt.

A partir de maintenant, quand vous créez une nouvelle classe qui n’a pas de parent, faites la TOUJOURS hériter de object.

Des tas de fonctions (par exemple les properties) fonctionnent beaucoup mieux avec les new-style classes. (D’ailleurs à partir de Python 3, elles sont activées par défaut)

Ainsi, notre classe Priere doit ressembler à ça maintenant :

class Priere(object):
 
    def __init__(self, expiation=False):
        self.expiation = expiation
 
    def prier(self, nombre_de_fois=1):
 
        for x in xrange(nombre_de_fois):
            for ligne in self.texte:
                if self.expiation:
                    print ligne.upper()
                else:
                    print ligne

Overriding

Parfois on veut tout le code du parent dans l’enfant. Parfois juste une partie. L’héritage vous permet de réécrire certaines méthodes du parent dans l’enfant, c’est ce qu’on appelle l’overriding.

Quand vous faites :

class Parent(object):
 
    def truc(self):
        print 'foo'
 
 
class Enfant(Parent):
 
    pass

En fait vous faites en quelque sorte :

class Enfant(object):
 
    def truc(self):
 
        print 'foo'

Si vous faites :

class Enfant(object):
 
    def truc(self):
 
        print 'foo'
 
    def truc(self):
 
        print 'bar'

Vous allez écraser la première méthode avec la deuxième, car elles portent toutes les deux le même nom :

>>> Enfant().truc()
bar

“Ecraser” en anglais, se dit “to override”. Félicitation, vous venez d’apprendre l’overriding ! Ce qu’on vient de voir plus haut s’écrit comme ça dans le cadre de l’héritage :

class Parent(object):
 
    def truc(self):
        print 'foo'
 
class Enfant1(Parent): 
    pass # pas d'overriding
 
class Enfant2(Parent):
 
    def truc(self):
        print 'bar' # overriding !
 
 
>>> Enfant1().truc()
foo
>>> Enfant2().truc()
bar

Enfant1 a tout le code du parent qui est copié. Enfant2 aussi, mais il réécrit la méthode, donc sa version de la méthode écrase celle du parent.

Voyons ce que ça donne sur un cas plus concret dans la vie de tous les jours comme les prières chrétiennes (promis je fais les sourates du Coran si je fais un tuto Haskell) :

class Priere(object):
 
    def __init__(self, expiation=False):
        self.expiation = expiation
 
    def prier(self, nombre_de_fois=1):
 
        for x in xrange(nombre_de_fois):
            for ligne in self.texte:
                if self.expiation:
                    print ligne.upper()
                else:
                    print ligne
 
 
class PaterNoster(Priere):
 
    texte = ("Notre Père qui es aux cieux",
            "que ton nom soit sanctifié",
            "que ton règne vienne,",
            "que ta volonté soit faite",
            "sur la terre comme au ciel.",
            "Donne-nous aujourd’hui",
            "notre pain de ce jour,",
            "pardonne-nous nos offenses",
            "comme nous pardonnons aussi",
            "à ceux qui nous ont offensés",
            "et ne nous soumets pas à la tentation",
            "mais délivre-nous du mal.",
            "Amen")
 
 
# sur avemaria, on veut mettre en avant la version en araméen
# par l'araméen c'est trop cool (ça ressemble au langage des furlings
# dans stargate)
 
 
class AveMaria(Priere): 
 
    vf = ("Je vous salue, Marie pleine de grâce ;",
            "Le Seigneur est avec vous.",
            "Vous êtes bénie entre toutes les femmes",
            "Et Jésus, le fruit de vos entrailles, est béni.",
            "Sainte Marie, Mère de Dieu,",
            "Priez pour nous, pauvres pécheurs,",
            "Maintenant, et à l'heure de notre mort.",
            "Amen.")
 
    vo = ("ܡܠܝܬ ܛܝܒܘܬܐ",
          "ܡܪܢ ܥܡܟܝ",
          "ܡܒܪܟܬܐ ܐܢܬܝ ܒܢܫ̈ܐ",
          "ܘܡܒܪܟ ܗܘ ܦܐܪܐ ܕܒܟܪܣܟܝ ܡܪܢ ܝܫܘܥ",
          "ܐܘ ܩܕܝܫܬܐ ܡܪܝܡ ܝܠܕܬ ܐܠܗܐ",
          "ܨܠܝ ܚܠܦܝܢ ܚܛܝ̈ܐ",
          "ܗܫܐ ܘܒܫܥܬ ܘܡܘܬܢ",
          "ܐܡܝܢ܀")
 
 
    def prier(self, nombre_de_fois=1, version='vo'):
 
        for x in xrange(nombre_de_fois):
            for ligne in getattr(self, version, 'vo'):
                if self.expiation:
                    print ligne.upper()
                else:
                    print ligne
 
>>> AveMaria().prier()
ܡܠܝܬ ܛܝܒܘܬܐ
ܡܪܢ ܥܡܟܝ
ܡܒܪܟܬܐ ܐܢܬܝ ܒܢܫ̈ܐ
ܘܡܒܪܟ ܗܘ ܦܐܪܐ ܕܒܟܪܣܟܝ ܡܪܢ ܝܫܘܥ
ܐܘ ܩܕܝܫܬܐ ܡܪܝܡ ܝܠܕܬ ܐܠܗܐ
ܨܠܝ ܚܠܦܝܢ ܚܛܝ̈ܐ
ܗܫܐ ܘܒܫܥܬ ܘܡܘܬܢ
ܐܡܝܢ܀
>>> AveMaria().prier(2, 'vf')
Je vous salue, Marie pleine de grâce ;
Le Seigneur est avec vous.
Vous êtes bénie entre toutes les femmes
Et Jésus, le fruit de vos entrailles, est béni.
Sainte Marie, Mère de Dieu,
Priez pour nous, pauvres pécheurs,
Maintenant, et à l'heure de notre mort.
Amen.
Je vous salue, Marie pleine de grâce ;
Le Seigneur est avec vous.
Vous êtes bénie entre toutes les femmes
Et Jésus, le fruit de vos entrailles, est béni.
Sainte Marie, Mère de Dieu,
Priez pour nous, pauvres pécheurs,
Maintenant, et à l'heure de notre mort.
Amen.

Ici la classe AveMaria hérite de la classe Priere. Deux méthodes sont copiées de Priere vers AveMaria : __init__ et prier().

__init__ ne change pas. Donc AveMaria a toujours le __init__ de Priere. Par contre, on a overridé prier() dans AveMaria, qui est maintenant un code personnalisé.

Ceci nous permet donc de bénéficier d’une partie du code en commun (__init__), et de choisir un comportement différent pour d’autres bout du code (prier()).

La classe PaterNoster, elle, n’est pas affectée. Elle n’override rien, et sa méthode prier() est la même que celle de Priere.

Peut-être voulez-vous une exemple plus terre à terre (et moins dans les cieux) :

Prenez la bibliothèque path.py, par exemple. Normalement quand on additionne deux strings, ça les concatène. Mais quand on les divise, le comportement par défaut est de lever une erreur.

>>> '/home/sam' + '/blog'
'/home/sam/blog'
>>> '/home/sam' / '/blog'
Traceback (most recent call last):
  File "<ipython-input-58-8701a4e82b4b>", line 1, in <module>
    '/home/sam' / '/blog'
TypeError: unsupported operand type(s) for /: 'str' and 'str'

Path.py override ce comportement :

import os
 
class path(str): # hé oui, on peut hériter des types de base
 
    def __div__(self, other): # override le comportement face à l'opérateur '/'
 
        return path(os.path.join(self, other))
 
... p = path('/home/sam')
... print p / 'blog'
/home/sam/blog

Ca nous donne une très jolie interface pour la manipulation des chemins d’accès.

Polymorphisme et autres diableries

Comme d’hab en prog, on adore les mots qui font super hype de la vibes du flex.

Le polymorphisme fait partie de ces termes compliqués qui cachent une notion simple : avoir une API commune qui fait des choses différentes.

Voyez-vous, nos deux classes AveMaria et PaterNoster, sont toutes les deux filles de la même classe parente. Elles se ressemblent donc beaucoup : elles ont des attributs et des méthodes en commun :

>>> p1 = AveMaria()
>>> p2 = PaterNoster()
>>> p1.expiation
False
>>> p2.expiation
False
>>> p1.prier
<bound method AveMaria.prier of <__main__.AveMaria object at 0x20618d0>>
>>> p2.prier
<bound method PaterNoster.prier of <__main__.PaterNoster object at 0x2061c90>>
>>> p1.__init__
<bound method AveMaria.__init__ of <__main__.AveMaria object at 0x20618d0>>
>>> p2.__init__
<bound method PaterNoster.__init__ of <__main__.PaterNoster object at 0x2061c90>>

Ce sont pourtant des classes différentes (et prier() ne fait pas du tout le même chose), mais elles partagent ce qu’on appelle une API (une interface), c’est à dire une manière de les utiliser.

Cette capacité à être utilisé pareil, mais produire un résultat diffférent est ce qu’on appelle le polymorphisme (et c’est la base du duck typing). Concrètement ça veut dire que vous pouvez utiliser les deux classes dans le même contexte, sans vous soucier de si c’est l’une ou l’autre :

>>> prieres = [AveMaria(), AveMaria(), PaterNoster(), AveMaria(), PaterNoster()]
>>> prieres
[<__main__.AveMaria object at 0x2061d50>, <__main__.AveMaria object at 0x2061d90>, <__main__.PaterNoster object at 0x2061f50>, <__main__.AveMaria object at 0x2061f90>, <__main__.PaterNoster object at 0x2061fd0>]
>>> for priere in prieres: 
...     priere.prier() # prier une prière, ça se fait pareil
 
ܡܠܝܬ ܛܝܒܘܬܐ
ܡܪܢ ܥܡܟܝ
ܡܒܪܟܬܐ ܐܢܬܝ ܒܢܫ̈ܐ
ܘܡܒܪܟ ܗܘ ܦܐܪܐ ܕܒܟܪܣܟܝ ܡܪܢ ܝܫܘܥ
ܐܘ ܩܕܝܫܬܐ ܡܪܝܡ ܝܠܕܬ ܐܠܗܐ
ܨܠܝ ܚܠܦܝܢ ܚܛܝ̈ܐ
ܗܫܐ ܘܒܫܥܬ ܘܡܘܬܢ
ܐܡܝܢ܀
ܡܠܝܬ ܛܝܒܘܬܐ
ܡܪܢ ܥܡܟܝ
ܡܒܪܟܬܐ ܐܢܬܝ ܒܢܫ̈ܐ
ܘܡܒܪܟ ܗܘ ܦܐܪܐ ܕܒܟܪܣܟܝ ܡܪܢ ܝܫܘܥ
ܐܘ ܩܕܝܫܬܐ ܡܪܝܡ ܝܠܕܬ ܐܠܗܐ
ܨܠܝ ܚܠܦܝܢ ܚܛܝ̈ܐ
ܗܫܐ ܘܒܫܥܬ ܘܡܘܬܢ
ܐܡܝܢ܀
Notre Père qui es aux cieux
que ton nom soit sanctifié
que ton règne vienne,
que ta volonté soit faite
sur la terre comme au ciel.
Donne-nous aujourd’hui
notre pain de ce jour,
pardonne-nous nos offenses
comme nous pardonnons aussi
à ceux qui nous ont offensés
et ne nous soumets pas à la tentation
mais délivre-nous du mal.
Amen
ܡܠܝܬ ܛܝܒܘܬܐ
ܡܪܢ ܥܡܟܝ
ܡܒܪܟܬܐ ܐܢܬܝ ܒܢܫ̈ܐ
ܘܡܒܪܟ ܗܘ ܦܐܪܐ ܕܒܟܪܣܟܝ ܡܪܢ ܝܫܘܥ
ܐܘ ܩܕܝܫܬܐ ܡܪܝܡ ܝܠܕܬ ܐܠܗܐ
ܨܠܝ ܚܠܦܝܢ ܚܛܝ̈ܐ
��ܫܐ ܘܒܫܥܬ ܘܡܘܬܢ
ܐܡܝܢ܀
Notre Père qui es aux cieux
que ton nom soit sanctifié
que ton règne vienne,
que ta volonté soit faite
sur la terre comme au ciel.
Donne-nous aujourd’hui
notre pain de ce jour,
pardonne-nous nos offenses
comme nous pardonnons aussi
à ceux qui nous ont offensés
et ne nous soumets pas à la tentation
mais délivre-nous du mal.
Amen

Le polymorphisme, c’est donc l’utilisation de l’héritage pour faire des choses différentes, mais en proposant la même interface (ensemble de méthodes et d’attributs) pour le faire. Le polymorphisme s’étend aussi à la réaction aux opérateurs (+, -, /, or, and, etc), d’autant qu’en Python, c’est implémenté avec les méthodes nommées avec __.

Un dernier point pour les gens qui se demandent ce que veut dire overloader. L’overload est lié à l’override et au polymorphisme, mais ce n’est pas la même chose : elle consiste à overrider plusieurs fois une même méthode avec une signature différente. Il n’y a pas d’overload en Python, la notion ne nous concerne donc pas. Si vous voulez en apprendre plus, chopez un tuto Java ou C++.

Qui est qui

Avec l’héritage vient la notion de type. Quand vous créez une classe, vous créez un nouveau type. Quand vous sous-classez, vous créez un sous-type.

C’est intéressant, car une classe fille, est de son propre type ET du type de son parent (mais l’inverse n’est pas vrai).

Avec Python, on vérifie cela avec isinstance() :

>>> isinstance(Priere(), Priere) # instance de sa propre classe
True
>>> isinstance(Priere(), str) # pas l'instance d'une classe quelconque
False
>>> isinstance(Priere(), AveMaria) # pas l'instance d'un enfant
False
>>> isinstance(AveMaria(), AveMaria) # instance de sa propre classe
True
>>> isinstance(AveMaria(), Priere) # instance de sa classe parente
True

C’est utile, car certains comportements sont basés sur le type. Le plus important étant le mécanisme des exceptions. Un try / except arrêtera l’exception demandée, ou du même type.

class MonExceptionPerso(Exception):
    pass
 
 
class FillesDeMonExceptionPerso(MonExceptionPerso):
    pass
 
 
try:
    raise MonExceptionPerso('Alerte ! Alerte !')
except MonExceptionPerso:
    print 'Exception arrêtée'
 
 
try:
    raise FillesDeMonExceptionPerso('Alerte ! Alerte !')
except FillesDeMonExceptionPerso:
    print 'Exception arrêtée'
 
 
try:
    raise FillesDeMonExceptionPerso('Alerte ! Alerte !')
except MonExceptionPerso:
    print 'Exception arrêtée'    
 
 
 
try:
    raise MonExceptionPerso('Alerte ! Alerte !')
except FillesDeMonExceptionPerso:
    print 'Exception arrêtée'    
 
 
Exception arrêtée
Exception arrêtée
Exception arrêtée
Traceback (most recent call last):
  File "<ipython-input-67-7e7aec1e78c9>", line 21, in <module>
    raise MonExceptionPerso('Alerte ! Alerte !')
MonExceptionPerso: Alerte ! Alerte !

MonExceptionPerso est de type MonExceptionPerso, donc l’exception est arrêtée. FillesDeMonExceptionPerso est de type FillesDeMonExceptionPerso, donc l’exception est arrêtée. FillesDeMonExceptionPerso, qui hérite de MonExceptionPerso, et donc de type MonExceptionPerso est arrêtée.

En revanche, MonExceptionPerso n’est PAS de type FillesDeMonExceptionPerso. Donc elle n’est pas arrêtée.

Je le signale car il est très courant de faire des enfants de ValueError, IOError, IndexError, KeyError, etc. et de le lever dans son propre programme. Cela permet à l’utilisateur de son code de pouvoir attraper soit toutes les erreurs de son code en faisant un except sur l’enfant, soit attraper toutes les erreurs de type ValueError, IOError, IndexError, KeyError en faisant le except sur le parent. On laisse ainsi une marge de manoeuvre dans la gestion des erreurs.

Appeler la méthode de la classe parent

Bon, vous avez overridé une méthode du parent. Mais c’est une groooooooooooooossse méthode. Vous allez pas la réécrire en entier, si ?

class Escorte(object):
 
    def calculer_prix(self, heures, tarif):
 
        return heures * tarif
 
 
class EscorteDeLuxe(Escorte):
 
    def calculer_prix(self, heures, tarif, supplement):
 
        tarif =  heures * tarif
 
        return tarif + (tarif * supplement / 100)

Sur cet exemple éminement intellectuel, calculer_prix() est simple, donc tout réécrire dans EscorteDeLuxe n’est pas grave. Mais si c’était une Geisha, hein ? Avec un manuel en Japonais ?

Pour éviter ces problèmes de complexité liés à la globalisation, le perméabilisation des frontières et les sites de streaming, on peut appeler la méthode du parent, dans l’enfant, en utilisant super():

class EscorteDeLuxe(Escorte):
 
    def calculer_prix(self, heures, tarif, supplement):
 
        # ceci est une manière compliquée de faire Escorte.calculer_prix
        # et de récupérer le résultat
        tarif =  super(EscorteDeLuxe, self).calculer_prix(heures, tarif)
 
        return tarif + (tarif * supplement / 100)

C’est exactement la même chose que plus haut. Sauf qu’au lieu de copier / coller le code du parent, on l’appelle directement.

Je résume :

Je réformule : quand vous overridez la méthode du parent dans l’enfant, celle du parent ne disparait pas. Elle est remplacée uniquement dans l’enfant. Et vous pouvez toujours vous servir de la version du parent en utilisant super(ClassEncours, self).nom_de_method(arguments) si vous en avez besoin.

Bon, c’était un gros morceau, et je suis pas sûr de pas être allé trop fort. Donc laissez en comment les remarques sur ce qui pourrait être mieux expliqué. La prochaine fois, on verra des usages poussés comme la composition, la délégation et tous ces trucs d’un vrai code de production.

Quels gros sites sont faits en Django ?

samedi 2 février 2013 à 12:00

Souvent on me demande une liste de gros sites qui ont été créés avec Django. Il y a bien une liste ici mais ce ne sont pas vraiment des sites très célèbres.

Voici des exemples plus parlants que des pastebins :

Modèles Django et classes abstraites

vendredi 1 février 2013 à 13:40

Bonjour Sam & Max,

Je suis le génie de la l’ANPE. Enigma, tu m’as trouvé, et tu as le droit à 3 voeux.

J’ai bien aimé votre blog consacré a dynamiser la communauté francophone python et je voudrais vous poser une petite question.
En fait, Quand il s’agit d’écrire un modèle, ce dernier doit hériter de la classe models.Model

par exemple:
class Entry(models.Model):

je me demande pourquoi exactement models.Model ?
et pourquoi ne pas faire l’héritage directement comme ça:
class Entry(Model)


Une explication de la syntaxe d’héritage des modèles Django ? Qu’il en soit ainsi.

On peut faire les deux. C’est une question d’import.

Si tu fais:

from django import models

Tu importes le module models.

Alors si tu veux hériter de la classe Model du module models, tu dois préfixer la classe du module (c’est ce qu’on appelle un namespace) : class Entry(models.Model).

Si par contre tu fais :

from django.models import Model

Tu importes la classe Model. Tu peux donc l’utiliser directement : class Entry(Model)

Le choix entre l’un et l’autre est essentiellement une question de style. En Django, pour les modèles on utilise from django import models car on va généralement également utiliser beaucoup de champs (models.IntegerField, models.BooleanField, etc) et que ça évite de tous les importer.

Aussi, je voudrais savoir qu’est ce qu’une Abstract base class ? ( car je sais que c’est un peu ça mais j’arrive pas à faire le lien )


Je ne peux pas faire tomber amoureux, ressusciter les morts ni faire des classes abstraites en Python. Mais je peux t’expliquer pourquoi. Accordé !

Une Abstract Base Class n’existe pas vraiment en Python. Il y a bien un module abc pour les simuler en utilisant des metaclasses, mais il est peu probable que tu en aies jamais besoin.

Dans d’autres langages, les classes abstraites sont des classes “à trou”. C’est à dire qu’elles ne sont pas complètes. On en hérite pour récupérer une partie de leur comportement, et on écrit le reste.

Imagine une classe Vehicule, qui sert de classe de base à une classe Voiture et une classe Moto :

class Vehicule(object):
 
 
    def __init__(carburant):
 
        self.carburant = carburant

Ceci est une classe abstraite. Elle possède des éléments communs à tous les véhicules, mais ne sait pas rouler. Chaque enfant doit implémenter la méthode rouler() :

class Moto(Vehicule):
 
    def rouler(km):
 
        self.carburant -= km * 3
        print 'Brrrrrrrrrrrrrrrrrr'
 
 
class Voiture(Vehicule):
 
    def rouler(km):
 
        self.carburant -= km * 7
        print 'Vrouuuuuuum'

Une classe abstraite est donc une classe incomplète, utile pour rassembler du code commun aux autres classes, mais qui ne marche pas d’elle même.

Dans d’autres langages, ce concept est formalisé par des mots clés, et des vérifications. Pas en Python : toutes les classes sont égales.

Maintenant le concept des classes abstraites dans Django est une application particulière. Je pense que tu poses la question parce que tu as vu ça :

class Vehicule(models.Model):
 
    class Meta:
 
        abstract = True

Dans ce cas particulier, le principe est le même que plus haut, mais appliqué aux models.

L’ORM de django crée en base de donnée une table par classe qui hérite de models.Model. Ce n’est pas ce que tu veux pour Vehicule : Vehicule ne marche pas par elle même, aucun intérêt de créer une table pour elle.

Ainsi Django le formalise l’aspect “abstrait” avec un attribut. Si l’attribut abstract est sur True, Django ne créera pas de table en base de donnée pour ce modèle, car il sait que c’est une classe abstraite, et qui donc n’est pas destinée à être utilisée directement. Tous les attributs du modèle Vehicule qui correspondent à des champs seront copiés dans les enfants, et seront des champs de leurs tables respectives.

Je vous remercie d’avance, et j’aimerais bien si c’est possible que la réponse soit comme le prochain sujet comme ça tout le monde en profite.


Tes désirs sont des ordres, maître.

(Comme d’hab, personne ne fait le vœu de libérer le génie, hein, bande de connards !)

La vérité sur le nombre de vues

jeudi 31 janvier 2013 à 09:45

Que ce soit sur Youtube ou sur votre blog, le nombre de vues est généralement grossièrement surévalué. En effet, les compteurs sont généralement limités à une IP ou un cookie, mais avec une contrainte de temps. Passée cette contrainte, retourner sur la page compte pour une vue de plus.

Or, il y a beaucoup de raisons pour que ce nombre ne corresponde pas au nombre de “nouvelles vues” effectif :

Bref, il y a probablement la moitié de nos vues qui sont des vues “artificielles”, quel que soit le média.

 

Quelle valeur retourner quand on ne trouve rien en Python ?

mercredi 30 janvier 2013 à 10:25

Hier je me suis dit que maintenant je mettrai de la musique à écouter pendant le tuto. Parce que. Donc détendez-vous avez un petit morceau de K’s Choice :

Techniquement une fonction Python ne peut pas ne rien retourner. Il n’existe rien de telle qu’un procédure dans ce langage. Si vous ne donnez aucune valeur de retour, elle va retourner None.

>>> def metal():
...     1 + 1
...     
>>> print metal()
None

Ne rien retourner, c’est donc choisir de retourner None. Or la valeur de retour est un choix d’API, et va dicter comme les gens vont utiliser votre fonction. Il convient donc de bien choisir sa valeur de retour “à vide”.

Fonction qui produit un résultat

La fonction qui produit un résultat, par exemple on lui donne deux valeur, et elle calcule une troisième, est le cas le plus facile.

Si la valeur peut être calculée, on retourne la valeur. Sinon, c’est qu’il y a un problème avec les arguments. Comme Python n’est pas un langage compilé, on ne check pas la nature des arguments. De plus parfois ce n’est pas tant un problème de nature que de valeur (comme le domaine de définition en maths). Dans ce cas, le mieux est de lever une exception (souvent ValueError ou un descendant) :

def by_poison(arg1, arg2):
 
    # faire l'opération ici
 
    # ah, ça marche pas ?
 
    raise ValueError("{} et {} doivent être bidule truc sinon ça ne marche pas".format(arg1, arg2))

On évite la plupart du temps les valeurs qui veulent dire sémantiquement “mauvaise opération” comme NaN en Python. On préférera lever une exception. None est rarement une bonne idée ici.

Fonction qui cherche un résultat

Ce genre de fonction ne va pas lever une exception sous prétexte qu’elle n’a pas trouvé un truc. Ne pas trouver est un état tout à fait normal (à moins que la présence soit requise, mais dans ce cas lever l’exception ne doit pas être à ce niveau mais à celui plus haut de toute façon).

Si c’est une vérification de présence, on retourne juste True / False :

def is_inside(value, other_value):
 
    # vérifier que value est dans other value
 
    return True # ou False

Notez que si vous faites les choses proprement, il vaut mieux overrider __contains__ pour permettre d’utiliser in directement.

Si votre recherche doit retourner une liste d’items (comme les lignes dans une base de données), dans ce cas quand on ne trouve rien, il vaut mieux retourner une liste vide. Cela permet de faire une boucle for sur le résultat dans tous les cas sans vérification ni side effect.

def inignition(query):
 
    # chercher un truc, et si on trouve le retourner
 
    # et tout à la fin, au cas où
    return []

Si vous cherchez un élément en particulier (genre un nombre premier dans la liste donnée), le mieux est de retourner None si il ne fait pas partie des données retournable. Si None fait partie des données trouvable, on retourne au fait de lever des exceptions :(. Mais c’est qu’il y a un problème ailleurs, car None ne devrait pas être une donnée significative.

def fed(truc, bidule):
 
    # chercher un truc, et si on trouve le retourner
 
    # et tout à la fin, au cas où
    return None

Certains cas particuliers demandent de retourner le même type que celui recherché. Il n’y a pas de règle pour ça, il va falloir utiliser votre tête, pour faire quelque chose de cohérent.

Par exemple, rechercher la chaîne la plus longue dans une séquence doit-il retourner None si aucune n’est trouvée, ou une chaîne vide ? Il n’y a pas de bonne réponse, c’est selon la sémantique de votre appli. Si elle traite uniquement avec des types string, une chaîne vide peut avoir du sens. Si c’est la présence ou l’absence de chaîne la plus longue qui est importante, alors None sera plus approprié.

A l’inverse, des fonctions comme “trouver la position” ne doit pas retourner -1 comme dans la plupart des autres langages. -1 est en effet très significatif en Python (pour le slicing par exemple). Évitez donc d’utiliser le même type pour dire “rien n’est trouvé” si la valeur risque d’être significative. Dans ce cas choisissez None.

megalist[:littlelist.search(truc)] # si littlelist.search(truc) retourn -1 c'est la merde !

La bibliothèque Python a tendance à lever des exception à tout va :

>>> s = 'Tasse'
>>> s.index('p')
Traceback (most recent call last):
  File "<ipython-input-14-e5567af6b163>", line 1, in <module>
    s.index('p')
ValueError: substring not found

C’est un comportement acceptable, mais ce n’est pas le plus pratique. Vous allez voir pourquoi.

Valeur par défaut

Retourner None quand on ne trouve rien peut résulter en une API extrêmement agréable car l’action la plus utilisée comme contre mesure est d’avoir une valeur par défaut. Prenez pas exemple un dictionnaire. Récupérer un élement et assigner une valeur par défaut si il manque ressemble à ça avec une exception :

>>> dicos = {'petit larousse' : 'dans la tête', 'gros robert' : 'dans le derriere'}
>>> dicos['urban dictionary'] # un entrée inexistante lève l'exception KeyError
Traceback (most recent call last):
  File "<ipython-input-18-0d425e038e32>", line 1, in <module>
    dicos['urban dictionary']
KeyError: 'urban dictionary'
>>> try: # du coup on doit faire un try
...     ou_ca = dicos['urban dictionary']
... except KeyError:
...     ou_ca = 'sur le net'
...     
>>> ou_ca
'sur le net'

Mais Python permet un raccourcis pour ça, vraiment sympas :

>>> dicos.get('urban dictionary', 'sur le net')
'sur le net'

La méthode get() permet de récupérer une valeur, et si la clé n’existe pas, elle retourne le deuxième argument. Or, par défaut, le second argument est None :

>>> print dicos.get('urban dictionary')
None

Donc si vous avez une fonction qui cherche quelque chose et qui risque de ne pas trouver, il peut être très pratique de proposer une argument comme valeur de retour par défaut, et de le mettre à None :

def and_blind(cherche, default=None):
 
     if je_trouve_pas_cherche:
        return default