modifié le

Objet

1. Classe

Une classe définit un type d’objets. Une classe C se définit dans un fichier source C.py.

Définir une classe dans son propre fichier source est une règle de bonne pratique, qu’il est important de suivre, surtout lorsque l’on débute.

Par exemple, pour modéliser les nombres complexes, on commence par créer une classe Complex dans un fichier Complex.py :

fichier Complex.py
class Complex:
  pass
Le nom d’une classe commence par une majuscule. C’est là encore une convention qu’il est fortement conseillé de respecter (cf PEPS 8).

2. Instance

Utiliser une classe C se fait hors du fichier C.py, dans un autre fichier qui l’aura importé (comme on l’aurait fait pour un module).

Utiliser une classe, c’est d’abord créer des instances. Par exemple, instancier la classe Complex, c’est créer des objets de type Complex :

fichier main.py
#!/bin/env python3
from Complex import Complex

z1 = Complex()
z2 = Complex()

On peut ensuite initialiser ces 2 objets :

fichier main.py
...
z1.x = 1
z1.y = 2
print(z1)          # <__main__.Complex object at 0x7fd643130fec>

z2.x = 0
z2.y = -1
print(z2)          # <__main__.Complex object at 0x7fdfec643130>

Le code ci-dessus crée et initialise 2 attributs x et y (la partie réelle et la partie imaginaire) pour chaque instance.

Par définition, l’état d’un objet (à un instant donné), c’est l’ensemble des valeurs de tous ses attributs. Donc ici, l’état de z1 c’est {x:1,y:2}, et l’état de z2 c’est {x:0,y:-1}.

On peut ensuite calculer le module et l’argument d’un complexe avec 2 fonctions :

fichier main.py
#!/bin/env python3
from Complex import Complex

def module(z):
  return math.sqrt(z.x**2 + z.y**2)      ⚠️ (1)

def argument(z):
  return math.atan(z.y/z.x)              ⚠️ (1)

z = Complex()
z.x = 1                                  ⚠️ (1)
z.y = 2                                  ⚠️ (1)
print(module(z))
print(argument(z))
1 ⚠️ Accès aux attributs x et y depuis l’extérieur de l’objet, à éviter !

Une bonne pratique en POO est de ne pas accéder, de l’extérieur d’un objet, à ses attributs. Il est clair que l’exemple précédent ne respecte pas cette règle de bonne pratique. 😕

3. Méthode

On peut encapsuler une fonction dans un objet. La fonction ainsi encapsulée s’appelle une méthode. Encapsuler les 2 fonctions module() et argument() précédentes donne :

fichier Complex.py
import math

class Complex:
  def module(z):
    return math.sqrt(z.x**2 + z.y**2)  👍 (1)

  def argument(z):
    return math.atan(z.y/z.x)          👍 (1)
1 La méthode faisant partie intégrante de l’objet, elle accéde aux attributs de l’objet sans violer les préceptes de la POO.

On appelle ces méthodes de la façon suivante :

fichier main.py
#!/bin/env python3
from Complex import Complex

z = Complex()
z.x = 1                        ⚠️
z.y = 2                        ⚠️
print(Complex.module(z))       (1)
print(Complex.argument(z))     (1)
1 on spécifie que la fonction appartient à Complex
Par définition, les attributs et méthodes d’un objet ou d’une classe sont appellés indistinctement membres de l’objet ou de la classe.

3.1. Appeler une méthode

Complex.module(z) peut s’écrire plus simplement z.module(), donc le code précédent s’écrit plus clairement :

fichier main.py
#!/bin/env python3
from Complex import Complex

z = Complex()
z.x = 1                                  ⚠️
z.y = 2                                  ⚠️
print(z.module())                        # appel de méthode
print(z.argument())                      # appel de méthode
Soit x une instance et f une méthode, l’appel x.f() est interprété comme f(x). Plus généralement, l’appel x.f(a,b) est interprété comme f(x,a,b). Par conséquent, le nombre de paramètres d’une méthode est égal au nombre d’arguments +1.

3.2. Créer des méthodes pour accéder aux attributs

Pour faire disparaitre les 2 instructions restantes qui accèdent directement aux attributs x et y, il suffit de créer une nouvelle méthode qui réalise cette initialisation :

fichier Complex.py
import math

class Complex:
  def initialise(z,reel,imag):
    z.x = reel
    z.y = imag

  def module(z):
    return math.sqrt(z.x**2 + z.y**2)

  def argument(z):
    return math.atan(z.y/z.x)

Avec cela, on peut écrire un programme dans les règles de l’art :

fichier main.py
#!/bin/env python3
from Complex import Complex

z1 = Complex()
z1.initialise(1,2)

z2 = Complex()
z2.initialise(2,-1)

print(z1.module())
print(z2.argument())

3.3. self

Il est de coutume de nommer self le premier paramètre de chaque methode, ce qui donne au final :

fichier Complex.py
import math

class Complex:
  def initialise(self,reel,imag):
    self.x = reel
    self.y = imag

  def module(self):
    return math.sqrt(self.x**2 + self.y**2)

  def argument(self):
    return math.atan(self.y/self.x)

3.4. L’encapsulation

L’encapsulation, concept important en programmation orientée objet, consiste à rassembler données et services associés au sein d’une structure boite noire ou capsule spaciale : l’objet. De l’extérieur de l’objet, il n’est pas souhaitable d’accéder à ses attributs. De l’extérieur, on ne doit utiliser que ses méthodes (qui elles accèdent aux attributs).

Suivant les langages de programmation, plusieurs niveaux de contrôle d’accessibilité existent. Par exemple, C++ dispose de notion de membres privés, protégés et publics. Les membres sont privés par défaut : seul le concepteur de la classe y accède. Les membres protégés sont accessibles au concepteur et aux héritiers de la classe, mais pas aux utilisateurs (ceux qui créent des instances). Et les membres publics sont accessibles à tous.

Python ne propose pas de contrôle d’accessibilité : tous les membres sont publics. Il existe juste une convention qui stipule que tout membre dont le nom commence par _ doit être considéré comme privé. Par exemple :

class Complex:
  def initialise(self,x,y):
    self._x = x
    self._y = y
  ...

met en évidence que _x et _y sont privés, et que, par conséquent, écrire :

z = Complex()
print(z._x,z._y)   # oulala, ce n'est pas bien !

n’est pas conforme aux préceptes de la POO. Rien de plus : rien n’empêche de passer outre. Plus radical consiste à préfixer le nom du membre par __ :

class Complex:
  def initialise(self,x,y):
    self.__x = x
    self.__y = y
  ...
z = Complex()
print(z.__x,z.__y)   # ⛔ ⛔

Cette fois-ci, le programme sort :

AttributeError: 'Complex' object has no attribute '__x'
AttributeError: 'Complex' object has no attribute '__y'

3.5. Récapitulatif

Les membres d’un objet sont ses attributs et ses méthodes. Une méthode est une fonction interne à l’objet, destinée à intervenir sur ses attributs. L’ensemble des méthodes d’un objet implémente son comportement, au même titre que ses attributs implémentent son état. Toute méthode possède au moins un paramètre destiné à recevoir l’objet pour lequel elle est appelée (c’est le premier de la liste) . La convention est de le nommer self. self reçoit donc l’objet courant, qui permet à la méthode d’accéder à tous ses membres.

Hors de la classe, dans le programme principal, écrire z.module() (où z est une instance de la classe Complex) n’est qu’une manière élégante d’écrire Complex.module(z).

Pour une classe C, bien noter les 2 acteurs :

  • le concepteur de la classe, qui gère le code du fichier C.py, et qui par conséquent, dispose d’un accès total aux entrailles des objets de type C,

  • l’utilisateur de la classe, qui crée des instances de C sans connaitre les détails de l’implémentation : il importe le fichier C.py sans connaitre son contenu (pas de nécessité pour lui d’y jeter un oeil, et encore moins de le modifier).

Pour mettre au point une classe, le codeur prend alternativement le rôle de concepteur et d’utilisateur (classe créée par le concepteur, testée par l’utilisateur, corrigée par le concepteur, etc.)

4. Membres prédéfinis

De base, un objet dispose de membres pré-existants. Ceux-ci sont facilement reconnaissables, car leur nom est de la forme __xxx__.

__class__, __name__, __dict__, __doc__ sont des exemples d’attributs préexistants.

__init__(), __new__(), __str__() sont des exemples de méthodes préexistantes. Elles réalisent un traitement par défaut, et sont automatiquement appelées tout au long de l’existance de l’objet. Mais le programmeur peut les redéfinir (les surcharger) pour personnaliser leur action.

Cette forme de nom alambiquée (un nom encadré par 2 doubles underscores) est courante en Python. Elle a pour but de minimiser le risque de collision avec ce que le programmeur peut choisir comme noms pour ses créations (en général, le développeur ne choisit pas de nom de cette forme). On les surnomme les dunders (Double UNDERscores).

Exemple :

class C:
  """
    Ceci est classe vide
  """
  pass

if __name__=="__main__":
  c = C()
  print(f"{c.__class__.__name__=}\n",
        f"{c.__dict__=}\n",
        f"{c.__init__=}\n",
        f"{c.__doc__=}\n",
        f"{dir(c)=}")                  (1)
1 La fonction dir() liste les membres de l’objet passé en argument.

4.1. Constructeur

Le constructeur d’une classe est une méthode particulière nommée __init__().

Si elle existe, alors elle est automatiquement appelée à la création de chaque instance. Son but est d’initialiser l’objet en cours de création.

fichier Complex.py
import math

class Complex:
  def __init__(self,reel,imag):
    """Constructeur de la classe"""
    self.x = reel
    self.y = imag

  def module(self):
    return math.sqrt(self.x**2 + self.y**2)

  def argument(self):
    return math.atan(self.y/self.x)

Créer un constructeur avec paramètres contraint l’instanciation :

fichier main.py
from Complex import Complex

z = Complex()     # TypeError: __init__() takes exactly 3 arguments (1 given)
z = Complex(1,2)  # Ok, 2 arguments sont maintenant obligatoires

On constate qu’il n’est maintenant plus possible de créer des complexes sans les initialiser. C’est un avantage : les variables non initialisées sont des bombes à retardement.

Constructeur (constructor) n’est pas le terme le mieux choisi pour nommer la méthode __init__(), car celle-ci ne crée pas l’objet, elle l’initialise. Mais c’est le terme consacré dans la plupart des langages à objet. Initialisateur serait plus pertinent (car le véritable constructeur en python est la méthode __new__).

4.2. Destructeur

Symétriquement, le destructeur d’une classe est une méthode particulière nommée __del__().

Si elle existe, alors elle est automatiquement appelée à la destruction de chaque instance. Son but est d’offrir une dernière occasion de restaurer proprement l’environnement avant la disparition de l’objet : fermer un fichier qui a été ouvert au préalable, libérer une ressource allouée auparavant, etc.

4.3. Sérialisation

La sérialisation est le codage d’une information dans un format qui peut être facilement conservé et partagé. La sérialisation au format texte d’un objet est sa représentation sous forme de chaîne de caractères. Un objet qui possède, par nature, une existence cantonnée à la mémoire volatile de l’ordinateur, peut, via la sérialisation, s’en évader. L’objet peut ainsi échapper à l’application qui l’a créé, et persister au-delà de l’exécution de cette application : il peut être sauvegardé sur disque (et être restauré par la suite, dans une autre application), ou bien envoyé sur le réseau (pour être récupéré et ressuscité à l’autre bout). Sa durée de vie dépasse ainsi celle de l’application qui l’a créé. On appelle cela un objet persistant.

L’opération inverse, qui consiste, à partir d’une représentation textuelle, à recréer l’objet dans la mémoire de l’ordinateur, s’appelle la désérialisation.

Par exemple, le complexe 1+2i peut être sérialiser en "Complex(1,2)". Pour le conserver ou l’expédier, il suffit de sauvegarder ou d’envoyer sur le réseau cette chaîne de caractères.

Remarque

Sérialiser n’est pas toujours aussi simple. Par exemple, comment sérialiser l2 définie par :

l1 = [1,2,3]
l2  = [l1,l1]

et représentable comme :

seri1

et différente de l3 définie par :

l3 = [[1,2,3],[1,2,3]]

et représentable comme :

seri2

4.3.1. La méthode __repr__()

La fonction repr() appliquée à un objet est censée renvoyer sa sérialisation au format chaîne de caractères :

z = Complex(1,2)
  z_serial = repr(z)
print(z_serial)  # -> <__main__.Complex object at 0x7f9def1860f0>

Le résultat n’est pas convainquant, mais on peut le personnaliser en redéfinissant la méthode __repr__() :

class Complex;
  ...
  def __repr__(self):
    return f"Complex({self.x},{self.y}")

if __name__ == "__main__":
  z = Complex(1,2)
  z_serial = repr(z)
  print(z_serial)  # -> Complex(1,2)

Pour désérialiser l’objet, il suffit de faire :

z = eval(z_serial)

4.3.2. La méthode __str__()

A l’instar de repr(), la fonction str() appliquée à un objet est censée en donner une représentation au format chaîne de caractères, sans avoir autant d’exigence :

z = Complex(1,2)
print(str(z))  # -> <__main__.Complex object at 0x7f9def1860f0>

Le résultat n’est pas plus convainquant que précédemment, __str__ ayant pour valeur par défaut __repr__. Mais là encore, on peut le personnaliser en redéfinissant la méthode __str__().

La chaîne renvoyée par str() n’a pas pour but de recréer l’objet, mais simplement de le représenter de façon univoque. Et Python utilisera automatiquement (sans avertissement) cette fonction str() pour convertir un objet en string :

class Complex:
  ...
  def __str__(self):
    return f"{self.x}+{self.y}i")

if __name__ == "__main__":
  z = Complex(1,2)
  print(z)  # -> 1+2i

str() est utilisé par Python à chaque fois qu’il y a necessité de convertir un objet en string. Et en particulier, print(objet) est interprété comme print(str(objet)), comme le montre l’exemple ci-dessus.

repr() et str() sont donc censées renvoyer toutes les 2 une caractérisation de l’objet sous forme d’une chaîne de caractères. Celle fournit par str() est plutôt destinée à des humains (la chaine renvoyée doit être humainement compréhensible), alors que celle fournit par repr() est plutôt à destination des programmes, comme essaie de l’expliquer la doc de Python.

extrait de la doc Python

__repr__() doit renvoyer la représentation « officielle » en chaîne de caractères d’un objet. Tout est fait pour que celle-ci ressemble à une expression Python valide pouvant être utilisée pour recréer un objet avec la même valeur (dans un environnement donné). La valeur renvoyée doit être un objet chaîne de caractères. Si une classe définit __repr__() mais pas __str__(), alors __repr__() est aussi utilisée quand une représentation « informelle » en chaîne de caractères est demandée pour une instance de cette classe.

Cette fonction étant principalement utilisée à des fins de débogage, il est donc important que la représentation donne beaucoup d’informations et ne soit pas ambigüe.

extrait de la doc Python

__str__() doit renvoyer une chaîne de caractères « informelle » ou joliment mise en forme de représentation de l’objet. La valeur renvoyée doit être un objet string.

Cette méthode diffère de __repr__() car il n’est pas attendu que __str__() renvoie une expression Python valide : une représentation plus agréable à lire ou plus concise peut être utilisée.

Ainsi dans l’exemple précédent, pour z=Complex(1,2), str(z) renvoie la string "1+2i", alors que repr(z) renvoie la string "Complex(1,2)".