La programmation orientée objet¶
La programmation orientée objet (POO) est un paradigme, c’est-à-dire une façon particulière de concevoir des systèmes logiciels. La POO envisage un logiciel comme des interactions entre une myriade d’entités élémentaires que l’on nomme des objets. Un objet est défini par un état qui est constitué de propriétés et par des comportements que l’on appelle des méthodes.
Un des apports importants de la POO est de permettre de concevoir un système logiciel en des termes directement issus de la problématique que le système est sensé résoudre. Par exemple, si le système est sensé gérer des comptes utilisateur, il est possible de créer des objets qui modélisent le comportement et les états d’un compte utilisateur. Par exemple, l’état d’un compte utilisateur pourra inclure un numéro de compte et une date de création. Il pourra avoir des comportements permettant de le verrouiller ou de le valider.
La POO est donc une façon de répondre à une problématique avec un modèle qui est plus proche des utilisateurs que de la représentation interne de l’information. Il est également plus aisé d’appréhender des systèmes complexes comme étant des composés d’objets aux comportements plus simples.
La classe et les instances¶
Un objet suit un modèle (un patron) qui définit ses propriétés et ses
comportements. Ce modèle est appelé une classe et se définit en Python avec
le mot-clé class
.
class MaClasse:
pass
Une classe est définie par un nom et un bloc précisant la définition de la classe.
Si on ne veut rien préciser de particulier dans le bloc, il faut utiliser le
mot-clé pass
.
Note
En Python, le nom des classes commence par convention par une lettre en majuscule. Les différents mots sont également indiqués par une lettre majuscule suivant le principe de l’écriture en dromadaire (camel case). Cette convention a évolué au cours du temps et donc il est possible de trouver des anciennes classes dont le nom commence par une lettre minuscule.
Pour créer un objet, il suffit d’appeler la classe :
mon_objet = MaClasse()
La variable mon_objet
référence l’objet crée. On dit que l’objet est une
instance de MaClasse
. Une classe définit un nouveau type dans le langage.
La POO est d’abord un modèle de programmation qui permet d’ajouter dans le
langage des nouveaux types d’objet. En Python, il existe deux fonctions standards
intéressantes pour connaître et tester le type d’un objet :
type(o)
Retourne le type de l’objet passé en paramètre.
>>> type(mon_objet) <class '__main__.MaClasse'>
isinstance(o, cls)
Retourne
True
si l’objet passé en premier paramètre est une instance de la classe passée en deuxième paramètre.>>> isinstance(mon_objet, MaClasse) True
Note
Nous verrons dans un chapitre ultérieur, que pour Python, tout est objet.
Ainsi les fonctions type(o)
et
isinstance(o, cls)
sont utilisables même avec des nombres :
>>> nombre = 1
>>> type(nombre)
<class 'int'>
>>> isinstance(nombre, int)
True
Les attributs¶
Un objet est défini par son état interne formé par l’ensemble de ses attributs.
Supposons que nous voulions représenter la notion de vecteur dans un espace
à deux dimensions dans notre système. Nous allons déclarer une classe Vecteur
.
class Vecteur:
pass
Nous pouvons maintenant créer autant de vecteurs que nous le souhaitons en leur
attribuant à chacun une valeur pour ses attributs x
et y
:
vec1 = Vecteur()
vec1.x = 2
vec1.y = 3
vec2 = Vecteur()
vec2.x = -12
vec2.y = vec1.x
print(f"Vecteur 1 : ({vec1.x}, {vec1.y})")
# Affiche Vecteur 1 : (2, 3)
print(f"Vecteur 2 : ({vec2.x}, {vec2.y})")
# Affiche Vecteur 2 : (-12, 2)
L’opérateur .
permet d’accéder à un attribut, soit pour le consulter, soit
pour le modifier. Chaque objet possède ses propres valeurs pour ses attributs.
C’est pour cela que l’on dit que les attributs permettent de définir l’état interne
de l’objet.
Prudence
Si vous essayez d’accéder à un attribut qui n’existe pas pour en consulter
la valeur, l’opération échoue avec une erreur de type AttributeError
.
>>> vec3 = Vecteur()
>>> vec3.x = 1
>>> vec3.y = 2
>>> x = vec3.x
>>> y = vec3.y
>>> z = vec3.z
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Vecteur' object has no attribute 'z'
On peut supprimer un attribut grâce au mot-clé del
.
>>> vec = Vecteur()
>>> vec.x = 1
>>> vec.y = -3
>>> del vec.x
>>> vec.x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Vecteur' object has no attribute 'x'
Il est possible pour un programme de découvrir et de manipuler les attributs
d’un objet grâce aux fonctions hasattr(o, n)
,
getattr(o, n)
, setattr(o, n, v)
et
delattr(o, n)
qui permettent respectivement de savoir si
un objet dispose d’un attribut, d’obtenir la valeur de cet attribut, de positionner
la valeur d’un attribut et de supprimer un attribut.
>>> vec = Vecteur()
>>> hasattr(vec, "x")
False
>>> setattr(vec, "x", 1)
>>> hasattr(vec, "x")
True
>>> getattr(vec, "x")
1
>>> delattr(vec, "x")
>>> hasattr(vec, "x")
False
L’intérêt de ces fonctions est qu’elles permettent de manipuler un attribut en donnant son nom sous la forme d’une chaîne de caractères. Cela ouvre la possibilité à un programme d’accéder aux attributs sans avoir réellement besoin de disposer de la déclaration de la classe au moment de l’écriture du programme. En Python, il est donc possible de manipuler les types comme des données et on parle alors de programmation réflexive ou de réflexivité.
Note
La fonction getattr(o, n)
accepte également un troisième
paramètre qui donne la valeur à retourner si l’attribut n’existe pas plutôt
que de produire une erreur de type AttributeError
.
>>> vec = Vecteur()
>>> getattr(vec, "attribut_qui_n_existe_pas", "Valeur par défaut")
'Valeur par défaut'
Les attributs implicites¶
Il existe des attributs implicites (c’est-à-dire pour lesquels il n’est pas nécessaire de fournir une valeur) pour chaque objet.
__doc__
Cet attribut contient la documentation de la classe. Pour renseigner cet attribut, il suffit d’ajouter une chaîne de caractères directement après la ligne de déclaration du nom de la classe :
class Vecteur: """Un vecteur à deux dimensions""" passLa documentation peut être affichée dans la console Python grâce à la fonction
help()
. Pour sortir du mode d’aide, il faut généralement appuyer sur la touche Q (pour quit).>>> v = Vecteur() >>> help(v)On peut aussi simplement accéder à l’attribut
__doc__
:>>> v.__doc__ 'Un vecteur à deux dimensions'
__class__
Cet attribut contient l’objet qui décrit la classe d’un objet. En Python même les classes sont représentées par des objets ! C’est cet attribut qui est notamment utilisé pour les fonctions
type()
etisinstance()
.
__module__
Cet attribut contient le nom du module auquel l’objet appartient.
__dict__
Cet attribut est le dictionnaire des attributs de l’objet.
>>> v = Vecteur() >>> v.x = 2 >>> v.y = 3 >>> v.__dict__ {'y': 3, 'x': 2}Cet attribut est notamment utilisé par la fonction
vars()
qui permet de créer un dictionnaire à partir d’un objet :>>> vars(v) {'y': 3, 'x': 2}Cet attribut permet également de mettre à jour tous les attributs à partir d’un dictionnaire.
>>> v = Vecteur() >>> v.__dict__ = {'y': 8, 'x': 3} >>> v.x 3 >>> v.y 8En Python, il est donc assez facile de passer d’un objet à un dictionnaire et d’un dictionnaire à un objet.
Les méthodes¶
Les méthodes représentent les comportements des objets. Elles sont décrites dans la classe en suivant les mêmes règles d’écriture que les fonctions.
Si on désire ajouter la possibilité de calculer la norme du vecteur, on peut
ajouter la méthode calculer_norme
.
import math
class Vecteur:
def calculer_norme(self):
return math.sqrt(self.x**2 + self.y**2)
Pour calculer la norme d’un vecteur, nous avons besoin de la fonction
sqrt()
que nous obtenons en important le module math
. La
méthode calculer_norme
prend un premier paramètre qui est appelé par convention
self
. self
représente l’objet dans le corps de la méthode. Il est
donc possible d’accéder aux attributs self.x
et self.y
pour réaliser
le calcul :
>>> v = Vecteur()
>>> v.x = 2
>>> v.y = 3
>>> v.calculer_norme()
3.605551275463989
Pour appeler une méthode, on utilise l’opérateur .
entre la variable qui
désigne l’objet et la méthode.
Note
Pour simplifier, nous pourrions dire qu’une méthode est une fonction qui
appartient à l’espace de nom de la classe et dont le premier paramètre est
l’objet lui-même. Ainsi l’écriture avec l’opérateur .
est une manière plus simple et raccourcie d’appeler une méthode mais
nous pouvons tous aussi bien écrire en Python :
>>> v = Vecteur()
>>> v.x = 2
>>> v.y = 3
>>> Vecteur.calculer_norme(v)
3.605551275463989
On comprend mieux ainsi la présence obligatoire du paramètre self
dans
la déclaration d’une méthode pour représenter l’objet.
En plus du paramètre self
, une méthode peut avoir d’autres paramètres
exactement comme une fonction. Nous pouvons ainsi ajouter une méthode pour calculer
le produit scalaire entre le vecteur courant et un autre vecteur passé en paramètre :
import math
class Vecteur:
def calculer_norme(self):
return math.sqrt(self.x**2 + self.y**2)
def calculer_produit_scalaire(self, v):
return self.x * v.x + self.y * v.y
>>> v1 = Vecteur()
>>> v1.x = 2
>>> v1.y = 3
>>> v2 = Vecteur()
>>> v2.x = 1
>>> v2.y = -1
>>> v1.calculer_produit_scalaire(v2)
-1
Une méthode peut se contenter de modifier l’état interne d’un objet (ses attributs)
et ainsi agir comme une procédure. Par exemple, la méthode normaliser
pour
la classe Vecteur
ne produit pas de résultat mais modifie les attributs
x et y pour garantir que la norme du vecteur est 1.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import math
class Vecteur:
def calculer_norme(self):
return math.sqrt(self.x**2 + self.y**2)
def calculer_produit_scalaire(self, v):
return self.x * v.x + self.y * v.y
def normaliser(self):
norme = self.calculer_norme()
self.x /= norme
self.y /= norme
|
Note
Notez comment la méthode normaliser()
appelle la méthode
calculer_norme()
à la ligne 12.
>>> v = Vecteur()
>>> v.x = 2
>>> v.y = 3
>>> v.normaliser()
>>> v.x
0.5547001962252291
>>> v.y
0.8320502943378437
>>> v.calculer_norme()
1.0
Le constructeur¶
L’implémentation de notre classe Vecteur
a un inconvénient majeur.
Toutes ses méthodes utilisent les attributs x
et y
pour réaliser leur
traitement. Que se passe-t-il si on oublie de donner des valeurs pour les attributs
x
et y
?
>>> v = Vecteur()
>>> v.calculer_norme()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
return math.sqrt(self.x**2 + self.y**2)
AttributeError: Vecteur instance has no attribute 'x'
Dans l’exemple ci-dessus, l’appel à la méthode calculer_norme()
produit
une erreur de type AttributeError
car l’objet n’a pas les attributs attendus.
Cela signifie que si un développeur veut utiliser correctement la classe
Vecteur
, il doit affecter une valeur pour les attributs x
et y
pour
chacun des objets de type Vecteur
.
Un constructeur est une méthode spéciale qui est appelée au moment de la création
de l’objet. Il permet de garantir que l’objet est dans un état cohérent dès
sa création. En Python, le constructeur s’appelle __init__()
est prend
comme premier paramètre l’objet en cours de création.
import math
class Vecteur:
def __init__(self):
self.x = 0
self.y = 0
def calculer_norme(self):
return math.sqrt(self.x**2 + self.y**2)
def calculer_produit_scalaire(self, v):
return self.x * v.x + self.y * v.y
def normaliser(self):
norme = self.calculer_norme()
self.x /= norme
self.y /= norme
Le constructeur de notre classe Vecteur
positionne à 0 les attributs nécessaires
aux objets de ce type. Nous avons maintenant corrigé notre problème.
>>> v = Vecteur()
>>> v.x
0
>>> v.y
0
>>> v.calculer_norme()
0
Même si un constructeur est une méthode un peu particulière, il peut accepter autant de paramètres que nécessaire. La valeur de ces paramètres est donnée au moment de la création de l’objet.
import math
class Vecteur:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def calculer_norme(self):
return math.sqrt(self.x**2 + self.y**2)
def calculer_produit_scalaire(self, v):
return self.x * v.x + self.y * v.y
def normaliser(self):
norme = self.calculer_norme()
self.x /= norme
self.y /= norme
>>> v = Vecteur()
>>> v.x, v.y
(0, 0)
>>> v = Vecteur(1, 2)
>>> v.x, v.y
(1, 2)
>>> v = Vecteur(y=6, x=12)
>>> v.x, v.y
(12, 6)
Les propriétés¶
Les attributs permettent de définir l’état d’un objet mais il existe
d’autres données qui peuvent être représentatives de cet état. Pour reprendre
notre exemple du vecteur, la norme d’un vecteur est également une donnée qui
est simplement calculée à partir des coordonnées x et y. L’ensemble de ces
données sont appelées les propriétés de l’objet. Si on souhaite créer une
propriété calculée, on peut le faire grâce au décorateur @property
.
import math
class Vecteur:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
@property
def norme(self):
return math.sqrt(self.x**2 + self.y**2)
def calculer_produit_scalaire(self, v):
return self.x * v.x + self.y * v.y
def normaliser(self):
norme = self.norme
self.x /= norme
self.y /= norme
En transformant le calcul de la norme en une propriété norme
, il est possible
d’y accéder comme s’il s’agissait d’un attribut :
>>> v = Vecteur(2, 3)
>>> v.norme
3.605551275463989
Par défaut, une propriété créée grâce à ce décorateur n’est pas modifiable.
Pour indiquer les méthodes qui doivent être appelées si on souhaite modifier
ou supprimer la propriété, il faut utiliser le même nom de méthode et ajouter
des décorateurs de la forme <nom>.setter
et <nom>.deleter
.
class Personne:
def __init__(self, prenom, nom):
self.prenom = prenom
self.nom = nom
@property
def nom_complet(self):
"""Prénom et nom de la personne"""
return f"{self.prenom} {self.nom}"
@nom_complet.setter
def nom_complet(self, nom_complet):
self.prenom, self.nom = nom_complet.split(maxsplit=1)
@nom_complet.deleter
def nom_complet(self):
self.nom = self.prenom = ""
>>> p = Personne("David", "Gayerie")
>>> p.nom_complet
'David Gayerie'
>>> p.nom_complet = "Eric Gayerie"
>>> p.prenom
'Eric'
>>> del p.nom_complet
>>> p.nom_complet
' '
Les attributs de classe¶
Une classe peut également avoir des attributs. Pour cela, il suffit de les déclarer dans le corps de la classe. Les attributs de classe sont accessibles depuis la classe elle-même et sont partagés par tous les objets. Si un objet modifie un attribut de classe, cette modification est visible de tous les autres objets.
class ClasseAvecCompteur:
nb_instances = 0
def __init__(self):
ClasseAvecCompteur.nb_instances += 1
>>> ClasseAvecCompteur.nb_instances
0
>>> c1 = ClasseAvecCompteur()
>>> c2 = ClasseAvecCompteur()
>>> c3 = ClasseAvecCompteur()
>>> ClasseAvecCompteur.nb_instances
3
Dans cet exemple simple, l’attribut de classe nb_instances
est incrémenté
à chaque fois que le constructeur de la classe est appelé. Donc cela permet
de compter le nombre d’objets de ce type créés par le programme.
Prudence
Notez comment l’attribut de classe nb_instances
est modifié dans le
constructeur de la classe ClasseAvecCompteur
. On utilise le nom de la
classe et non self
car sinon il s’agirait de l’attribut de l’objet.
Il est possible d’utiliser self
pour lire le contenu d’une variable
de classe mais cela est fortement déconseillé car cela peut donner lieu à
une ambiguïté à la lecture du code.
Les attributs de classe sont le plus souvent utilisés pour représenter des constantes.
Méthodes de classe¶
Tout comme il est possible de déclarer des attributs de classe, il est également
possible de déclarer des méthodes de classe. Pour cela, on utilise le décorateur
@classmethod
. Comme une méthode de classe appartient à une
classe, le premier paramètre correspond à la classe. Par convention, on appelle
ce paramètre cls
pour préciser qu’il s’agit de la classe et pour le distinguer
de self
.
class ClasseAvecCompteur:
nb_instances = 0
@classmethod
def reinitialiser_compteur(cls):
cls.nb_instances = 0
def __init__(self):
ClasseAvecCompteur.nb_instances += 1
>>> c1 = ClasseAvecCompteur()
>>> c2 = ClasseAvecCompteur()
>>> c3 = ClasseAvecCompteur()
>>> ClasseAvecCompteur.nb_instances
3
>>> ClasseAvecCompteur.reinitialiser_compteur()
>>> ClasseAvecCompteur.nb_instances
0
Méthodes statiques¶
Une méthode statique est une méthode qui appartient à la classe mais qui
n’a pas besoin de s’exécuter dans le contexte d’une classe. Autrement dit,
c’est une méthode qui ne doit pas prendre le paramètre cls
comme premier
paramètre. Pour déclarer une méthode statique, on utilise le décorateur
@staticmethod
. Les méthodes statiques sont des méthodes
utilitaires très proches des fonctions mais que l’on souhaite déclarer dans le
corps d’une classe.
class UneClasse:
@staticmethod
def une_methode():
print("appel de la méthode")
>>> UneClasse.une_methode():
# Affiche appel de la méthode
Visibilité des attributs et des méthodes¶
Certains langages de programmation proposent des mécanismes pour gérer la visibilité des attributs et des méthodes. Il arrive souvent qu’une classe déclare des méthodes et des attributs qui ne sont pas destinés à être directement appelés par un programme. On dit qu’il s’agit de méthodes et d’attributs privés, c’est-à-dire destinés à être utilisés uniquement par l’objet lui-même. Par exemple, un objet peut stocker dans un attribut une connexion vers une système externe (base de données, serveur web…) avec pour objectif d’offrir une interaction simplifiée avec ce système. Cet attribut n’est pas sensé être accédé depuis l’extérieur de l’objet. En le rendant privé, on évite au reste du programme d’y accéder inutilement. Cela permet également une meilleure évolutivité du programme en évitant de trop exposer les données internes d’un objet. C’est ce que l’on appelle le principe d’encapsulation.
En Python, il n’existe pas de mécanisme dans le langage qui nous permettrait
de gérer la visibilité. Par contre, il existe une convention dans le nommage.
Une méthode ou un attribut dont le nom commence par _
(underscore) est
considéré comme privé. Il est donc déconseillé d’accéder à un tel
attribut ou d’appeler une telle méthode depuis l’extérieur de l’objet.
class ClientSystemeExterne:
def ouvrir_connexion(self):
self._connexion = ConnexionInterne()
def fermer_connexion(self):
if self._connexion is not None:
self._connexion.fermer()
self._connexion = None
Fermer la liste des attributs¶
Nous avons vu au début de ce chapitre que nous pouvons créer autant d’attributs
que nous le souhaitons pour un objet. Cela peut conduire à des bugs ou des
difficultés de compréhension du code. Si nous reprenons notre classe Vecteur
,
nous pouvons écrire :
1 2 3 4 | >>> v = Vecteur(1, 0)
>>> v.z = 4
>>> v.norme
1.0
|
À la ligne 2, le programme positionne la valeur d’un nouvel attribut z
. Un
lecteur pourrait penser que le vecteur est en fait un vecteur à trois dimensions.
Mais il n’en est rien. La classe Vecteur
n’a pas été conçue pour prendre en
charge cette troisième dimension et la norme du vecteur sera bien de 1.
Pour éviter ce genre de situation, il est possible déclarer la liste finie des
attributs. Pour cela, on déclare une attribut de classe appelé
__slots__
.
import math
class Vecteur:
__slots__ = ('x', 'y')
def __init__(self, x=0, y=0):
self.x = x
self.y = y
@property
def norme(self):
return math.sqrt(self.x**2 + self.y**2)
def calculer_produit_scalaire(self, v):
return self.x * v.x + self.y * v.y
def normaliser(self):
norme = self.norme
self.x /= norme
self.y /= norme
Si maintenant un programme essaie de positionner la valeur d’un attribut qui
ne fait pas partie de la définition de la classe, l’interpréteur génère une
erreur AttributeError
.
1 2 3 4 5 | >>> v = Vecteur(1, 0)
>>> v.z = 4
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Vecteur' object has no attribute 'z'
|
Note
L’attribut __slots__
a également une utilité pour l’optimisation
du code. Comme on indique à l’interpréteur le nombre exact d’attributs, il
peut optimiser l’allocation mémoire pour un objet de ce type.
Créer des objets avec des attributs constants¶
Lorsque l’on veut créer des objets qui contiennent uniquement des attributs
en lecture seule, il est possible d’utiliser la classe collections.namedtuple
.
Elle fournit une solution à la déclaration de constante en Python.
>>> from collections import namedtuple
>>> Intervalle = namedtuple('Intervalle', ('min', 'max'))
>>> intervalle = Intervalle(min=0, max=100)
>>> intervalle.min
0
>>> intervalle.max
100
>>> intervalle.min = 12
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
La classe namedtuple
attend en paramètre le nom du type
à créer ainsi que la liste des noms des attributs. L’objet construit a les mêmes
comportements qu’un tuple, c’est-à-dire que ses données ne peuvent pas être modifiées
mais elles peuvent être accessibles comme des attributs. Si on tente de modifier
la valeur d’un des attributs, cela produit une erreur de type AttributeError
.