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


Prérequis :

Nous avons vu les attributs de classe et les propriétés, mais de manière très rudimentaire. Il y a beaucoup de choses à savoir sur la question.

Attribut de classe, la totale

Nous avons vu qu’un attribut de classe était accessible par la classe, sans instance :

>>> class TrueLies:
...
...         attribut_de_classe = 'valeur'
...
>>> print TrueLies.attribut_de_classe
valeur

Mais également par une instance :

>>> print TrueLies().attribut_de_classe
valeur

C’est simple et direct. Il n’y a pas de public static virtuel neoconceptuel à mettre devant comme dans certains langages trop cafféinés.

Ce qu’on a pas vu par contre, c’est ce qui arrive si on modifie la valeur de ces attributs.

Pour faire simple :

  • Si je modifie l’attribut au niveau de l’instance, seule l’instance voit les modifications.
  • Si je modifie l’attribut au niveau de la classe, la classe et toutes les instances crées après ont la nouvelle valeur.
>>> instance = TrueLies()
>>> instance.attribut_de_classe = 'nouvelle valeur'
>>> print instance.attribut_de_classe
nouvelle valeur
>>> print TrueLies.attribut_de_classe # ça ne change rien pour la classe
valeur
>>> TrueLies.attribut_de_classe = 'encore une nouvelle valeur'
>>> print TrueLies.attribut_de_classe
encore une nouvelle valeur
>>> print instance.attribut_de_classe # ça ne change rien pour cette instance
nouvelle valeur
>>> print TrueLies().attribut_de_classe # mais pour une nouvelle oui
encore une nouvelle valeur

Comme self est l’instance en cours, vous pouvez remplacer instance par self dans l’exemple ci-dessus (dans une méthode bien sûr, pas dans le shell), ça marche pareil.

Vu comme ça, ça à l’air d’être super pour donner une valeur par défaut à des attributs.

C’est une mauvaise idée.

En effet, les attributs de classes sont initialisés une seule fois, à la première lecture du code par Python. Pour les objets immutables comme les strings ou les int, on s’en branle. Mais pour les objets mutables comme les dicos ou les listes, c’est la merde.

>>> class TrueLies:
...
...         attribut_de_classe = [] # mettons une liste ici
...
...
>>> flim_pas_sur_le_cyclimse = TrueLies()
>>> etranger_qui_vole_le_travail_des_francais = TrueLies()

On a donc deux instances de TrueLies. A priori, on a aussi deux valeurs de attribut_de_classe, une pour chaque instance ?

Des faux seins sur un FAUX de norman fait des vidéos

Norman va bientôt faire un film avec les robins des bois au fait

>>> id(etranger_qui_vole_le_travail_des_francais.attribut_de_classe)
28992072
>>> id(flim_pas_sur_le_cyclimse.attribut_de_classe)
28992072

Comme vous pouvez le voir c’est le même id. attribut_de_classe est la même chose, car Python initialise attribut_de_classe une seule fois pour toute la session. Les deux instances ont donc un attribut qui contient une référence (puisque TOUT est référence en Python) qui pointe sur la même liste.

Dans les fait, ça veut dire que si on modifie l’un, ben forcément la modification est visible de l’autre côté aussi :

>>> etranger_qui_vole_le_travail_des_francais.attribut_de_classe.append(1)
>>> flim_pas_sur_le_cyclimse.attribut_de_classe
[1]

C’est rarement ce qu’on veut.

Donc ne donnez pas de valeur par défaut dans les attributs de classe. Faites ça avec __init__:

>>> class TrueLies:
...
...         def __init__(self):
...
...                 self.attribut_tres_classe = []
 
>>> flim_pas_sur_le_cyclimse = TrueLies()
>>> etranger_qui_vole_le_travail_des_francais = TrueLies()
>>> flim_pas_sur_le_cyclimse.attribut_tres_classe.append(1)
>>> flim_pas_sur_le_cyclimse.attribut_tres_classe
[1]
>>> etranger_qui_vole_le_travail_des_francais.attribut_tres_classe
[]

Le comportement attendu est le bon ici.

Ok, mais alors ils servent à quoi ces attributs à deux cents, là, cousin ?

– Et ben wesh, tu vois, ils servent à stocker les constantes et le cache.

– Ouais quand j’ai constamment du stock j’ai du cash, man.

– héééééééééééé… xactement.

Donc, d’abord, on met les pseudo constantes en attributs de classe. Je dis pseudo car il n’existe pas de constantes en Python. On parle de constante quand une variable est écrite toute en majuscule, une convention pour dire “le programme ne change jamais la valeur de cette variable (et t’as pas intérêt à la changer, pigé ?)”.

class TrueLies:
 
    NOMBRE_DE_BALLES_DANS_LE_CHARGEUR = 10000

On peut être certain que Schwarzy ne recharge jamais (y a que Thierry Lhermitte qui ferait un truc aussi ringard). Donc le nombre de balles dans le chargeur ne varie pas. On le met comme attribut de classe. C’est essentiellement à titre informatif. Pour nous ça change rien, mais pour quelqu’un qui va utiliser le code, il sait qu’il peut chercher toutes les constantes liées à TrueLies en faisant TrueLies.LES_TRUCS_EN_MAJUSCULES.

C’est l’usage le plus courant.

On peut aussi utiliser les attributs de classes pour créer une API déclarative, comme le fait l’ORM de Django. Mais bon, c’est mega advanced, donc je mets de côté le how to. Le jour où vous coderez un truc comme ça, vous lirez pas un article pour débutant.

Enfin on peut utiliser les attributs de classe pour partager des données entre les instances. Par exemple du cache.

class TrueFalseSomeWhatLiesMaybeWhoKnows :
 
    _cache = {}
 
 
    def calculer_le_nombre_de_balles_a_la_seconde(scene):
 
        # si y a rien dans le cache, on fait le calcul et on le met dans le cache
        if scene not in self._cache :
 
            self._cache[scene] = # mettre un calcul bien compliqué ici
 
        # on retourne le contenu du cache
        return self._cache[scene]
 
>>> TrueFalseSomeWhatLiesMaybeWhoKnows().calculer_le_nombre_de_balles_a_la_seconde(1)
56586586
>>> TrueFalseSomeWhatLiesMaybeWhoKnows().calculer_le_nombre_de_balles_a_la_seconde(1)
56586586

Le premier appel va faire le calcul. Mais pas le second, car le résultat a été stocké dans le dictionnaire qui est au niveau de la classe (et donc chaque nouvelle instance a une référence vers ce dictionnaire) et peut donc être réutilisé.

Vous noterez qu’on a appelé la variable _cache et pas cache. C’est encore une convention. Ça signifie, “cette variable est utilisée en interne, t’as pas à savoir ce qu’elle fait, imagine que c’est privé. Circulez, y a rien à voir.” Les outils de complétion de code masquent ces variables par défaut.

Pour la culture, sachez que nommer ses variables __nom les rend “privées” en Python. Néanmoins cette fonctionnalité est facilement contournable, et très peu appréciée dans la communauté, donc évitez-la.

Et puis il y a les méthodes statiques aussi

On a vu les méthodes de classe, qui sont, comme les attributs de classe, des méthodes qui n’ont pas besoin d’instance pour être appelées :

class ArmeeDesDouzeSinges:
    """
        Ouais c'est un remake d'un film français aussi.
        Ça vous la coupe hein ?
    """
 
    SINGES = 12
 
    @classmethod
    def nombre_de_singes_au_carre(cls):
 
        return cls.SINGES * cls.SINGES
 
 
>>> ArmeeDesDouzeSinges.nombre_de_singes_au_carre()
144

Le premier paramètre n’est pas l’objet en cours mais la classe en cours, et c’est tout ce dont nous avons besoin ici pour accéder à SINGES puisque c’est un attribut de classe.

Cet exemple est issu d’un de mes codes réels de productions, vous pouvez en constater l’intérêt évident dans la vie de tous les jours.

Nan je déconne, moi j’utilise plutôt des dromadaires au quotidien.

Bon, mais sachez plus sérieusement qu’il y a en prime des méthodes de classe, des méthodes statiques. C’est la même chose, mais aucun paramètre n’est passé automatiquement.

class LHommeALaChaussureRouge:
    """
        Oui c'est exactement ce que vous pensez. C'est affligeant.
    """
 
    @staticmethod
    def crie_son_desespoir(): # pas de cls
 
        print "Nooooooooooooooooooooooooooooooooon"
 
 
>>> LHommeALaChaussureRouge.crie_son_desespoir()
Nooooooooooooooooooooooooooooooooon

Alors là on arrive dans la feature anecdotique hein, du genre qu’on sort qu’au repas de Noël. Je la mets au menu juste pour que vous sachiez que ça existe, mais franchement je ne m’en sers presque jamais.

L’usage est le même qu’une méthode de classe, mais pour les trucs qui n’ont pas besoin d’avoir accès à la classe en cours.

Bon.

Bref, c’est juste une fonction préfixée. Avantage : elle est un poil plus rapide que la méthode de classe, et elle est mieux rangée qu’une simple fonction. Mais c’est vraiment tout.

Retour sur les properties

Si vous avez bonne mémoire, vous vous souviendrez qu’une propriété est juste un déguisement qu’on met sur une méthode pour qu’elle ressemble à un attribut :

class LesVisiteursEnAmerique:
    """
        Mais si, mais si...
    """
 
    @property
    def replique(self):
 
        return 'okay'
 
 
>>> film_pourri = LesVisiteursEnAmerique()
>>> film_pourri.replique
'okay'

Mais on peut aller plus loin que la lecture. On peut aussi décorer l’écriture et la suppression !

class LesVisiteursEnAmerique(object): # on rajoute (object) truc ici
 
 
    def __init__(self):
        self._replique = 'okay' # c'est privé, pas touche, y a un underscore
 
 
    @property
    def replique(self):
        print 'get'
        return self._replique
 
 
    @replique.setter # le décorateur a le même nom que la méthode
    def replique(self, value): # value est la valeur à droite du '=' quand on set
        print 'set to {}'.format(value)
        self._replique = value
 
 
    @replique.deleter
    def replique(self):
        print 'delete'
        self._replique = None
 
 
>>> film_pourri = LesVisiteursEnAmerique()
>>> film_pourri.replique
get
'okay'
>>> film_pourri.replique = 'zarma'
set to zarma
>>> film_pourri.replique
get
'zarma'
>>> film_pourri._replique
'zarma'
>>> del film_pourri.replique
delete
>>> film_pourri.replique
get

Et là vous allez me dire :

– c’est quoi ce (object) là haut là qui est apparu magiquement ?
– et à quoi ça sert ? Nom d’un remake hollywodien !

À la première question je dirai, réponse dans la prochaine partie. Sachez juste que s’il y a pas ce (object), le setter et le deleter ne marchent pas.

Pour la seconde, imaginez un truc un peu plus choupi, comme une réplique garantie d’être toujours en majuscule :

class LesVisiteursEnAmerique(object):
 
 
    def __init__(self):
        self._replique = 'OKAY'
 
 
    @property
    def replique(self):
        return self._replique
 
 
    @replique.setter
    def replique(self, value):
        self._replique = value.upper() # PAF ! on majusculise
 
>>> film_TRES_pourri = LesVisiteursEnAmerique()
>>> film_TRES_pourri.replique
'OKAY'
>>> film_TRES_pourri.replique = "D'ac"
>>> film_TRES_pourri.replique
"D'AC"

Et voilà le travail.

On peut donc “intercepter” la récupération, la suppression et la modification d’un attribut facilement. Cela permet d’exposer une belle API à base d’attribut, mais derrière faire des traitements complexes.

Il suffit de transformer un attribut normal en méthode, et de lui coller @property au cul.

C’est pour cette raison qu’on n’a jamais de getter et de setter en Python : on utilise les attributs tels quel, et si le besoin se présente, on en fait des propriétés.

Next stop, héritage, overriding, polymorphisme et autres gros mots que vous pourrez ressortir aux soirées mousses dans les hackerspaces.

24 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 3.

  • roro

    Sam, je voudrais savoir à combien tu estime le temps nécessaire à la pleine acquisition des trois tutos.
    x jours à y heures/jours de lecture/analyse (sans application/mise en pratique)
    Ou en nombre de lectures/analyses.
    Comme tu est malin, tu va me dire que ça dépend de chacun; mais comme tu est prof, tu devrais pouvoir me donner une moyenne.
    Fais-tu de l’application/mise en pratique dans le cadre de ton enseignement?
    PS: Tu a le droit de me dire que ça doit être assimilé en première lecture.

  • Sam Post author

    Dur dur, mais je vois le but de la question.

    Je dirais qu’il y a deux stades :

    – l’élève comprends
    – l’élève peut en tirer peinement parti

    Le premier stade, je dirais entre 45 minutes et 2 heures, selon le niveau de l’élève.

    Certains le feront en 20 minutes car ils ont des bases ailleurs qui les font aller plus vite, mais un grand débutant, même doué, ne peut pas aborder une notion abstraite complètement nouvelle en moins de 45 minutes, lecture comprise.

    De plus je dirais que l’élève qui met 2 heures n’a rien à se reprocher, ce n’est pas une notion facile, et la POO, ça ne clic pas pour tout le monde. Ce n’est pas une question d’intelligence, ça peut aller à l’encontre de son expérience ou ses habitudes de réflexions.

    Pour le deuxième stade, je dirais quelques mois.

  • arnaud

    Au vu de la réponse de Sam, va m’falloir au moins 6 mois pour mon cas désespéré, même si va falloir que je m’y mette dans le cadre de mon taf !
    huhu

  • Kontre

    Okay, alors :

    C’est simple et directe. => C’est simple et direct.
    les attributs de classes => les attributs de classe (plusieurs fois dans le texte)
    Il y a plusieurs films qui sont des flims dans les exemples. C’est fait exprès ?
    Le comportement attendu est le bon ici => Le comportement attendu est le bon ici. (il manque un point)
    Ouai quand j’ai constamment du stock => Ouais quand j’ai constamment du stock
    on met les pseudos constantes en attributs de classe. Je dis pseudos => on met les pseudo constantes en attributs de classe. Je dis pseudo
    en faisait TrueLies.LES_TRUCS_EN_MAJUSCULES. => en faisant TrueLies.LES_TRUCS_EN_MAJUSCULES.
    donc je met de côté le how to => donc je mets de côté le how to
    Par exemple du cache. => Par exemple du cache :
    Les outils de complétions de code masques ces variables => Les outils de complétion de code masquent ces variables
    nommer ses variable => nommer ses variables
    donc évitez là. => donc évitezla.
    Ouai c’est un remake => Ouais c’est un remake
    Ca vous la coupe => Ça vous la coupe
    cri_son_desespoir() => crie_son_desespoir() (deux fois)
    Je la met au menu => Je la mets au menu
    je ne m’en sert => je ne m’en sers
    pour le trucs => pour les trucs
    le decorateur a le même nom => le décorateur a le même nom
    A la première question je dirais => À la première question je dirai
    si il y a pas => s’il n’y a pas
    une réplique garanti => une réplique garantie
    qu’on a jamais de getter => qu’on n’a jamais de getter

    Voilà voilà…

    Je pensais que les attributs de classe était identiques pour toutes les instances, créées avant ou après les modifications. C’est bon à savoir. En même temps, je m’en sers encore moins que les @staticmethod, qui elles sont parfois pratiques pour avoir de jolies api.

  • roro

    @Sam. Merci pour la précision; ça permets de planifier, et de se situer dans la progression…Très utile.
    @Kontre. Boouuuyouhou! J’espère qu’il n’y a pas autant d’épluchures sur le fond que sur la forme.
    Je vais attendre le passage des différents correcteurs pour prendre cette page. Je n’ai que 3Go de forfait, et il y 740 mn d’attente pour le recharger.

  • Etienne

    @roro
    Pour t’aider à te faire une idée, je me suis mis à python (orienté développement web) début septembre dernier, càd il y a 6 mois, en autodidacte. Avant ça je l’avais utilisé deux fois pour des petits trucs (quelques opérations sur des listes). Et j’avais pour ainsi dire jamais programmé avant (deux ou trois boucles en basic au milieu des annés 80, et une histoire de choix aléatoire dans une liste en c++ il y a 10 ans).

    Là je lis les tutos de Sam sur la POO et j’apprends des trucs (pas grand’chose dans celui-ci), mais ce dont il traite principalement, je le sais déjà et je l’utilise (faut dire que j’en ai vraiment beaucoup appris ici).

    Bon, c’est pas toujours simple: comprendre les concepts de base, savoir créer ta petite classe à toi, jouer avec des ‘super’, @classmethod, créer un décorateur, comprendre pourquoi c’est pas une bonne idée de mette un mutable en valeur par défaut d’une méthode, tout ça c’est une chose. Après, bien l’utiliser pour écrire du code qui fonctionne et qui est clair et maintenable, c’est une autre histoire.

  • désanuseur

    Faut 10 ans pour dire “je commence à maîtriser un langage”, le reste c’est de la branlette…et en plus faut en avoir appris d’autres avant pour pouvoir comparer et avoir un certain recul sur leurs emplois respectifs. Il faut différencier application complexe et algo complexe, un bon programmeur sait faire de belles choses sur un programme simple de conception, un mauvais programmeur sait aussi programmer une application complexe mais de manière sale et lourde, là différence se situe là. Un langage s’apprend, la vivacité d’esprit nécessaire à réaliser un algo pour résoudre un problème ne s’apprend pas, on naît avec ou pas. On peut bien sûr arriver au même résultat mais cela sera plus douloureux, plus lent et plus sale.

  • Topic

    En lisant ce guide je viens de comprends un truc, je me dis tiens sur d’autres sites j’ai déjà vu une autre façon de faire pour les property , exemple sur ce site : http://www.qui-saura.fr/monBlog/property.html.

    J’étais donc parti pour faire un message c’est quoi la meilleur façon de faire mais en fait je viens de capter, c’est juste un décorateur d’une fonction integrée à python et du même coup @classmethod et autres @staticmethod qui pour moi avaient toujours été d’obscurs mots clés à connaître et ben c’est la même chose.

    Ouais enfin du coup maintenant j’ai un doute je viens de survoler les deux articles sur les décorateurs et je suis en train de me demander si je ne dis pas n’importe quoi parce que je ne vois pas d’exemples de ce style @replique.setter (@truc.machin). Je vais allez me relire à fond les pages sur les décorateurs.

  • Etienne

    @désanuseur

    On peut bien sûr arriver au même résultat mais cela sera plus douloureux, plus lent et plus sale.

    Pas nécessairement plus douloureux, ni plus sale. Juste plus lent.

    “Le lièvre et la tortue”, tu connais?

  • Sam Post author

    @Topic : non non tu as tout pigé. C’est juste que @replique.setter est une astuce : @property attache la fonction setter() à la fonction replique() pour rendre l’utilisation plus pratique. Car oui, en Python, les fonctions peuvent avoir des attributs (car en fait les fonctions sont des objets).

  • Désanuseur

    ““Le lièvre et la tortue”, tu connais?”

    Je te laisse donc expliquer le concept à mon chef ! Il va adorer ! ;)

  • Sam Post author

    Parcequ’il faut alors utiliser un hack pour y accéder. Or la plupart des codes sont imparfaits.

    Il est très probable (et fréquent) que quelqu’un utilise ton code pour quelque chose ou d’une manière que tu n’as pas prévu. Un bon programmeur ouvrira un rapport de bug. Mais il a quand même besoin du bon comportement maintenant tout de suite.

    Donc il va bidouiller ton code pour le réparer. Sauf que si tu as des variables __, il aura le choix entre faire un hack très moche ou éditer ton code directement. Par contre, si tu n’as pas de variable cachée, il peut faire un petit monkey patching et rouler jeunesse.

  • foxmask

    Alors si c’est si chiant pourquoi le laisser possible en python ?

  • Réchèr

    Ouais, mais dans True Lies, Schwarzy bute un crocodile en le traitant de “sac à main”, ce que Lhermitte ne fait pas. C’était donc un remake qui valait le coup.

    Quelques petites corrections :

    “public static virtuel neconceptuel
    C’est quoi ce “neconceptuel” ? C’est un vrai-mot clé, ou c’est une faute de frappe au mot “neoconceptuel” ?

    Y’a dû y avoir confusion dans la lolisation du code. Les instances qui s’appellent “etranger_qui_vole_le_travail_des_francais” devraient être renommées en “flim_pas_sur_le_cyclimse“.
    À remplacer à deux endroits.

  • sensini42

    ce qu’on a pas vu : on n‘a ?
    toutes les instances crées : créées
    ça à l’air : ça a

    écrite toute en majuscule : tout en majuscule (voire tout en lettres capitales, majuscule = 1e lettre du mot toussa)

    (object) là haut là: balise code à object + là-haut avec trait d’union

    Très clair sinon :)

  • John

    Hello et bravo pour ce tuto, j’en suis à ma seconde lecture.

    Je maitrise bien les notions des classes Python, mon grand défaut est de vouloir (insconciemment) créer des classes outils pour y fourrer un peu tout ce qui a un rapport (donc finalement beaucoup de méthodes).

    J’essaie donc de sortir de mon habitude bizarre de tout ranger, et d’utiliser un peu plus les objets et les comportements naturels qui y sont liés, et après plusieurs années de dev procédural ce n’est pas toujours simple (surtout sur la partie intéraction entre les classes, j’hésite souvent entre la dérivation etc…)

    As-tu un conseil qui peut aider à réfléchir objet ?

    Autre question, un peu plus technique, les attributs de classes au niveau de la classe se mettent-ils à jour si 2 objets sont dans des threads différents ?

  • Sam Post author

    As-tu un conseil qui peut aider à réfléchir objet ?

    Réfléchir objet le moins possible. On utilise les objets quand on veut créer une API particulière ou quand on a un état auquel on fait référence dans de nombreuses fonctions qu’on transforme donc en méthode. Pour le reste, pas besoin d’objets.

    Autre question, un peu plus technique, les attributs de classes au niveau de la classe se mettent-ils à jour si 2 objets sont dans des threads différents ?

    Oui les espaces mémoires sont partagé entre les threads, pour avoir des variables non partagées, il faut utilise des thread locals variables ou des queues.

  • Benji

    Si je modifie l’attribut au niveau de la classe, la classe et toutes les instances crées après ont la nouvelle valeur.

    Bizarre, j’ai ça :

    >>> class HobJay:
    ...  classtribut = 'et un'>>> 
    ...
     
          >>> h1 = HobJay()
          >>> h2 = HobJay()
          >>> h3 = HobJay()
          >>> h1.classtribut
          'et un'
          >>> h2.classtribut
          'et un'
          >>> h3.classtribut
          'et un'
          >>> h2.classtribut = 'et deux'
          >>> h2.classtribut
          'et deux'
          >>> HobJay.classtribut = 'et trois zero !'
          >>> h1.classtribut
          'et trois zero !'
          >>> h2.classtribut
          'et deux'
          >>> h3.classtribut
          'et trois zero !'
          >>> import sys
          >>> print(sys.version)
          3.4.3 (default, Mar 25 2015, 17:13:50)
     
          [GCC 4.9.2 20150304 (prerelease)]

    On dirait plutôt que changer l’attribut de classe change l’attribut de toutes les instances, sauf celles dont l’attribut a été changée avant. Comme si l’attribut de classe était “écrasé” par un attribut d’instance chez h2.

    Ou alors c’est un comportement qui change avec les versions ?

  • Sam Post author

    Il va falloir que je rajoute le mécanisme de recherche.

    Quand tu fais obj.attr, ça va chercher obj.attr, et si ça ne le trouve pas, ça va chercher Class.attr, puis si ça ne le trouve pas, ça va chercher celui de la classe parente, etc.

    Donc si tu set ton attribut (obj.attr = truc), il existe sur l’objet, et c’est ce qui est retourné. Si tu ne le set pas, alors c’est celui de la classe qui est utilisé, et c’est pourquoi tu vois la modification sembler se “propager”.

Comments are closed.

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