PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Mise à jour

Mise à jour de la base de données, veuillez patienter...

Pourquoi self en Python ?

mercredi 2 juillet 2014 à 06:44

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 :

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 :

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

flattr this!

Error happened! 0 - count(): Argument #1 ($value) must be of type Countable|array, null given In: /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php:428 http://ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/?Pourquoi-self-en-Python #0 /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php(999): VroumVroum_Blog->update() #1 /var/www/ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/index.php(1): require_once('...') #2 {main}