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
:
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
:
#!/bin/env python3
from Complex import Complex
z1 = Complex()
z2 = Complex()
On peut ensuite initialiser ces 2 objets :
...
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 :
#!/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 :
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 :
#!/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 :
#!/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 :
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 :
#!/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 :
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
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.
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 :
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 |
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.
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 :

et différente de l3 définie par :
l3 = [[1,2,3],[1,2,3]]
et représentable comme :

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
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
Cette méthode diffère de |
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)"
.