Pourquoi self en Python ? 22


Quand on écrit une méthode dans une classe en Python, vous êtes obligé de faire ceci :

class UneClasse:
   #              ?
   #              |
   #              v
   def __init__(self):
      self.attribut = 'value'
 
   #                ?
   #                |
   #                v
   def une_methode(self):
      print(self.attribut)

Vous êtes tenu de déclarer self, le premier paramètre, qui sera l’instance en cours.

Cela étonne, parfois irrite. Pourquoi dois-je me taper ce self ?

D’abord, petite clarification : le nom self n’est qu’une convention. Le premier paramètre de toutes les méthodes est une instance, soit, mais il n’a pas de nom obligatoire.

Ce code marche parfaitement :

class UneClasse:
 
   def __init__(tachatte):
      tachatte.attribut = 'value'
 
   def une_methode(tachatte):
      print(tachatte.attribut)

Il ne passera probablement pas une code review, mais il est valide.

Il ne passera pas une code review, non pas parce que tachatte n’est pas un nom de variable politiquement correcte – après tout ces mignonnes boules de poils ne sont-elles pas aimées par tous ? – mais parce que self est une convention forte. Tellement forte que les éditeurs de code la prennent en compte.

Mais je suppose que la plus grosse interrogation, c’est pourquoi on se tape le self à la main, et pas :

  • Rien comme en C++ ?
  • @ comme en ruby ?
  • this comme en JS ?
  • $this comme en PHP ?

Il y a de nombreuses raisons.

D’abord, rien comme le C++ ne permettrait pas, en Python, de distinguer une variable locale d’une variable d’un scope supérieur, rendant la lecture difficile. La philosophie de Python étant qu’on lit un code 100 fois plus qu’on l’écrit et qu’il faut donc faciliter la lecture plutôt que l’écriture, cela n’a pas été retenu.

@ comme en Ruby suppose 3 notations. @ pour les variables d’instance, @@ pour les variables de classe, et self pour l’instance en cours (avec un usage aussi pour définir les méthodes de classe car les classes sont des instances, mais je trouve ça super bordélique). Ça introduit beaucoup de mécanismes supplémentaires pour utiliser quelque chose qui existe déjà, et comme en la philosophie de Python c’est qu’il ne devrait y avoir qu’un seul moyen, de préférence évident, de faire quelques chose, utiliser juste une référence aux classes et aux instances a été choisi.

Pour le JS, et son binding de merde, je vais passer mon tour, sinon je vais encore m’énerver.

Reste donc la solution de PHP, Java, etc., une référence explicite this, mais automatiquement présente dans le scope de la méthode.

La réponse courte, est encore une fois philosophique. En Python, on préfère l’explicite plutôt que l’implicite.

Si vous avez ce code :

class UneClasse:
 
   def __init__(self):
      self.attribut = 'value'
 
   def une_methode(self):
      print(self.attribut)

Et que vous faites :

instance = UneClasse()
instance.une_methode()

En réalité vous faites sans le savoir :

instance = UneClasse()
UneClasse.une_methode(instance)

L’interpréteur fait la conversion pour vous (il y a derrière une notion de bound/unbound, mais c’est un autre sujet).

A l’appel de la “méthode”, instance est visiblement présente, c’est assez explicite, et plus court que la version traduite par l’interpréteur. Donc Python vous aide avec cette traduction. Mais au niveau de la déclaration de la méthode, il n’y a pas de mention explicite de la référence à la variable d’instance, donc Guido a choisi, comme en Modula-3, de rendre le passage explicite.

Ce comportement a tout un tas de conséquences forts pratiques.

Python a en effet une fonctionnalité que PHP et Java n’ont pas : l’héritage multiple. Dans ce contexte, le passage explicite du self permet de facilement choisir l’appel de la méthode d’un parent, sans faire appel à des mécanismes supplémentaires (C++ ajoute par exemple un opérateur pour ça):

class Clerc:
   heal = 50
   def soigner(self):
      return self.heal * 2
 
class Paladin:
   heal = 60
   def soigner(self):
      return self.heal * 1.5 + 30
 
class BiClasse(Clerc, Paladin):
   heal = 55
   def soigner(self):
      # Hop, j’appelle les parents distinctement
      # et fastochement en prenant la méthode
      # au niveau de la classe, et en lui passant
      # manuellement l'instance.
      soin_clerc = Clerc.soigner(self)
      soin_palouf = Paladin.soigner(self)
      return (soin_clerc + soin_palouf) / 2

Mais, et peu de gens le savent, il permet aussi de faire de la composition beaucoup plus fine, en ignorant complètement l’héritage.

On peut notamment créer des algo globaux, et ensuite les attacher à des objets:

 
# Une fonction moyenne qui fonctionne de manière générique
# et utilisable normalement. Imaginez que cela puisse être
# un algo complexe. On veut pouvoir l'utiliser hors du
# cadre d'objet.
def moyenne(sequence):
   """ Calcule la moyenne d'une séquence.
 
       Les décimales sont tronquées
   """
   notes = list(sequence)
   return sum(notes) / len(notes)
 
 
class DossierEleve:
 
   # Intégration de l'algo de la fonction "moyenne"
   # à notre dossier, sans se faire chier à faire
   # un héritage. Comme 'self' est passé en premier
   # paramètre, 'sequence' contiendra 'self'. Comme
   # plus bas on rend le dossier itérable, tout va
   # marcher.
   moyenne = moyenne
 
   def __init__(self):
      self.notes = []
 
   # on rend le dossier itérable
   def __iter__(self):
      return iter(self.notes)
 
   # on donne une taille au dossier
   def __len__(self):
      return len(self.notes)
 
# On peut l'intégrer à plusieurs classes.
class CarnetDeClasse:
 
   moyenne = moyenne
 
   def __init__(self):
      self.notes = []
 
   def __iter__(self):
      return iter(self.notes)
 
   def __len__(self):
      return len(self.notes)
 
c = CarnetDeClasse()
c.notes = [12, 14, 13, 15]
print(c.moyenne())
## 13
e = DossierEleve()
e.notes = [9, 8, 17, 1]
print(e.moyenne())
## 8

Vous allez me dire, pourquoi ne pas faire moyenne(eleve) dans ces cas ? Parce que ce code supposerait connaitre de l’implémentation d’élève. Alors que eleve.moyenne() utilise le code encapsulé, sans avoir à s’en soucier. Si le code change (ce qui arrive dans des cas plus complexes qu’une moyenne), pas besoin de changer son API.

Vous me direz, si on avait pas le self explicite, on pourrait faire ça :

class DossierEleve:
   def moyenne(self):
      return moyenne(self)

Mais :

  • C’est plus verbeux.
  • C’est un look up supplémentaire.
  • On perd l’introspection de moyenne().
  • On perd la docstring de moyenne().
  • On perd tous les attributs attachés à moyenne() (si moyenne est un objet avec une méthode __call__, vous n’y avez plus accès depuis l’extérieur).
  • Si la fonction avait plus de paramètres, il faut lui repasser à la main. Moyenne() est une fonction très simple.
  • Si moyenne fait de l’instrospection de stack, vous allez fausser son analyse.
  • Ça rajoute un step si on lance le debugger. Et une ligne inutile dans la stack trace.
  • Ça ne marche pas si moyenne est une fonction éphémère, dynamiquement créée, car on n’a pas de référence à la fonction et le code va planter.

En Python 3, ça va même plus loin. Ce self explicite permet également de partager du code entre objets, sans utiliser l’héritage.

Imaginez un autre scénario, où vous importez une lib gestion_classe.py, qui n’est pas votre code. Vous savez que son algo de calcul de moyenne est très complexe, mais très rapide, et efficace, et vous voulez en bénéficier. Seulement, il est encapsulé dans la classe CarnetDeClasse, et en faire hériter un profil d’élève d’un carnet de classe n’a absolument aucun sens.

Dans gestion_classe.py :

class CarnetDeClasse:
 
   def __init__(self):
      self.notes = []
 
   def __iter__(self):
      return iter(self.notes)
 
   def __len__(self):
      return len(self.notes)
 
   def moyenne(self):
      notes = list(self)
      return sum(notes) // len(notes)

Et dans votre code :

 
from gestion_classe import CarnetDeClasse
 
class DossierEleve:
 
   moyenne = CarnetDeClasse.moyenne
 
   def __init__(self):
      self.notes = []
 
   # on rend le dossier itérable
   def __iter__(self):
      return iter(self.notes)
 
   # on donne une taille au dossier
   def __len__(self):
      return len(self.notes)
 
e = DossierEleve()
e.notes = [9, 8, 17, 1]
print(e.moyenne())
## 8

22 thoughts on “Pourquoi self en Python ?

  • swordofpain

    Il me semble que dans le dernier exemple, on devrait plutôt faire comme suit pour le résultat attendu :

    class DossierEleve:
        moyenne = CarnetDeClasse.moyenne

    self n’est pas défini dans le corps de la classe si je ne m’abuse, et on cherche à récupérer la méthode et pas son résultat.

    Sinon, très bel article, comme d’hab !

  • Christophe31

    Même si c’est la même chose j’aurai abordé le monkey patching qui est aussi facilité. Aujourd’huis c’est surtout pour ça que j’ai utilisé le self, monkey patcher des libs pour avoir des dépendances propre sans avoir à maintenir mes versions.

  • Kaos Idea

    Tiens c’est marrant, pas plus tard qu’hier soir on en parlait avec un pote que l’utilisation de self faisait un peu polémique : des self partout c’est lourd à la longue.
    On avait soulevé plusieurs points comme quoi self reste cool comme dans ton article.
    Mais bon, tu as été au jusquauboutisme! Bel article.

  • Sam Post author

    @Christophe31: “monkey patching” et “propre” dans la même phrase… Mais tu as raison, ça aurait été un bon truc à rajouter et c’est bien utile pour les dépendances qui merdent.

  • chabotsi

    Juste pour dire que les moyennes sont inversées dans l’article ;)

    c = CarnetDeClasse()
    c.notes = [12, 14, 13, 15]
    print(c.moyenne())
    ## 13
    e = DossierEleve()
    e.notes = [9, 8, 17, 1]
    print(e.moyenne())
    ## 8
  • Heavy27z

    Super article (comme d’hab).
    J’apprends plus ici que nul part ailleurs :).

    Petite correction de rien du tout, si je ne me trompe pas :
    Dans le dernier exemple, le nom du fichier contenant la classe CarnetDeClasse devrait s’appeller gestion_classe.py, non ?

    Ca m’avait un peu perturbé.

  • Wploplin

    Pareil que chabotsi, une petite coquille :
    “Dans classroom.py :” -> “Dans gestion_classe.py :”

    Question bête, car je suis resté bloquer longtemps dessus:
    Est ce une bonne pratique d’ècrire :

    class CarnetDeClasse:
     
       <strong>moyenne = moyenne</strong>

    Car pour comprendre que le deuxième moyenne c’est un appel à la fonction en dehors de la classe, ouch … (après je suis un bon noob aussi)

    Et pour finir, pourquoi cette fonction moyenne n’est pas recursive?

    class DossierEleve:
       def moyenne():
          return moyenne(self)

    Est ce due uniquement au paramètrage non présent dans dans la fonction de la classe?

  • Sam Post author

    Je ferais plutôt un truc du genre :

    import module.function
     
    class Classe:
        method = module.function

    Pour que ce soit plus clair en effet.

    class DossierEleve:
       def moyenne(self):
          return moyenne(self)

    N’est pas un appel récursif car la méthode est dans le namespace de la classe. Pour un appel récursif:

    class DossierEleve:
       def moyenne(self):
          return self.moyenne()

    La première forme appelle la fonction du scope supérieur, la seconde appelle la fonction du scope de l’intérieur de la classe.

  • kontre

    D’ailleurs il manque un self au def moyenne(): ^^
    La fonction moyenne est appelée par moyenne(argument), alors que la méthode moyenne serait appelée self.moyenne(), dont y’a pas d’ambiguité et par de récursivité. D’où l’intérêt du self, justement.

    J’ai eu ce cas de figure y’a quelques jours, après hésitation j’ai fait une classe mixin et un héritage multiple parce que ça me paraissait plus propre dans mon cas, mais en tout cas je confirme que c’est une problématique qu’on rencontre en vrai !

  • Heavy27z

    T’as quand même de la chance que sur les com’ du blog, les gens sachent apprécier la qualité de l’article, en y ajoutant souvent des détails instructifs ou en corrigeant qq coquilles.

    Nan parce qu’ailleurs on croise souvent des autistes du Bescherelle… C’est vite saoulant.

  • kontre

    La vraie différence, c’est que les autistes du Bescherelle ont droit de modifier les fautes directement dans l’article ! Ça a l’ait très efficace comme méthode de modération des commentaires. Et ça laisse des possibilités aux correcteurs pour le premier avril… ^^

  • Gring

    Merci pour l’article !

    Je ne savais pas qu’il y avait de l’héritage multiple en Python. Du coup vous m’avez donné envie de m’y mettre, malgré cette syntaxe.

  • Fohan

    Merci pour votre explication claire et concise !
    Si seulement il y avait un blog FR dans votre lignée (qualité et humour) portant sur le ruby, je serai aux anges *_*

  • Zanguu

    Je confirme la correction de k4nar.
    Avec

    print Clerc().soigner()
    print Paladin().soigner()
    print BiClasse().soigner()

    J’obtiens :

    100
    120.0
    Traceback (most recent call last):
      File "", line 25, in 
      File "", line 18, in soigner
    TypeError: 'int' object is not callable

    alors qu’en remplaçant heal(self) par soigner(self) j’obtiens :

    100
    120.0
    111.25

Leave a comment

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