Classes et objets#
Cette page s’appuie sur le livre de Gérard Swinnen « Apprendre à programmer avec Python 3 » disponible sous licence CC BY-NC-SA 2.0. L’introduction à la programmation orientée objet est inspirée par le livre de Claude Delannoy « Programmer en Java » (Eyrolles) que vous êtes invités à consulter si vous souhaitez découvrir le langage Java.
Python est un langage qui permet la Programmation Orientée Objet (POO).
Brève introduction à la Programmation Orientée Objet#
Nous avons vu plusieurs types de base en Python (int
pour les entiers, float
pour les flottants, str
pour les chaînes de caractères, etc.). La notion de classe va en quelque sorte nous permettre de généraliser la notion de « type » afin de créer de nouvelles structures de données.
Une classe définit des attributs et des méthodes. Par exemple, imaginons une classe Voiture
qui servira à créer des objets qui sont des voitures. Cette classe va pouvoir définir un attribut couleur
, un attribut vitesse
, etc. Ces attributs correspondent à des propriétés qui peuvent exister pour une voiture. La classe Voiture
pourra également définir une méthode rouler()
. Une méthode correspond en quelque sorte à une action, ici l’action de rouler peut être réalisée pour une voiture.
Si on imagine une classe Avion
, elle pourra définir une méthode voler()
. Elle pourra aussi définir une méthode rouler()
. Par contre, la classe Voiture
n’aura pas de méthode voler()
car une voiture ne peut pas voler. De même, la classe Avion
pourra avoir un attribut altitude
mais ce ne sera pas le cas pour la classe Voiture
.
Après avoir présenté la notion de classe, nous allons voir la notion d”objet. On dit qu’un objet est une instance de classe. Si on revient à la classe Voiture
, nous pourrons avoir plusieurs voitures qui seront chacune des instances bien distinctes. Par exemple, la voiture de Jonathan, qui est de couleur rouge avec une vitesse de 30 km/h, est une instance de la classe Voiture
, c’est un objet. De même, la voiture de Denis, qui est de couleur grise avec une vitesse de 50 km/h, est un autre objet. Nous pouvons donc avoir plusieurs objects pour une même classe, en particulier ici deux objets (autrement dit : deux instances de la même classe). Chacun des objets a des valeurs qui lui sont propres pour les attributs.
Les notions de classe et d’objet#
Définition d’une classe Point
#
Voici comment définir une classe appelée ici Point
.
class Point:
"Definition d'un point geometrique"
Par convention en Python, le nom identifiant une classe (qu’on appelle aussi son identifiant) débute par une majuscule. Ici Point
débute par un P majuscule.
Création d’un objet de type Point
#
Point()
Ceci crée un objet de type Point
. En POO, on dit que l’on crée une instance de la classe Point
.
Une phrase emblématique de la POO consiste à dire qu”un objet est une instance de classe.
Il faut bien noter que pour créer une instance, on utilise le nom de la classe suivi de parenthèses. Nous verrons par la suite qu’il peut y avoir des arguments entre ces parenthèses.
Affectation à une variable de la référence à un objet
Nous venons de définir une classe Point
. Nous pouvons dès à présent nous en servir pour créer des objets de ce type, par instanciation. Créons par exemple un nouvel objet et mettons la référence à cet objet dans la variable p
:
>>> p = Point()
Avertissement
Comme pour les fonctions, lors de l’appel à une classe dans une instruction pour créer un objet, il faut toujours indiquer des parenthèses (même si aucun argument n’est transmis). Nous verrons un peu plus loin que ces appels peuvent se faire avec des arguments (voir la notion de constructeur).
Remarquez bien cependant que la définition d’une classe ne nécessite pas de parenthèses (contrairement à ce qui de règle lors de la définition des fonctions), sauf si nous souhaitons que la classe en cours de définition dérive d’une autre classe préexistante (ceci sera expliqué plus loin).
Nous pouvons dès à présent effectuer quelques manipulations élémentaires avec notre nouvel objet dont la référence est dans p
.
Exemple
>>> print(p)
<__main__.Point instance at 0x012CAF30>
Le message renvoyé par Python indique que p
contient une référence à une instance de la classe Point
, qui est définie elle-même au niveau principal du programme. Elle est située dans un emplacement bien déterminé de la mémoire vive, dont l’adresse apparaît ici en notation hexadécimale.
>>> print(p.__doc__)
Definition d'un point geometrique
On peut noter que les chaînes de documentation de divers objets Python sont associées à l’attribut prédéfini __doc__
:.
Exemple avec deux objets
a = Point()
b = Point()
La variable a
va contenir une référence à un objet.
>>> print(a)
<__main__.Point instance at 0x012CADC8>
De même b
va contenir une référence à un autre objet.
>>> print(b)
<__main__.Point instance at 0x012CAF08>
Nous avons ici 2 instances de la classe Point
(2 objets) :
la première à laquelle on fait référence au moyen de la variable
a
,la seconde à laquelle on fait référence au moyen de la variable
b
.
On fait bien ici la distinction entre classe et objet. Ici nous avons une seule classe Point
, et deux objets de type Point
.
Définition des attributs#
class Point:
"Definition d'un point geometrique"
p = Point()
p.x = 1
p.y = 2
print("p : x =", p.x, "y =", p.y)
L’objet dont la référence est dans p
possède deux attributs : x
et y
.
La syntaxe pour accéder à un attribut est la suivante : on va utiliser la variable qui contient la référence à l’objet et on va mettre un point .
puis le nom de l’attribut.
Exemple
class Point:
"Definition d'un point geometrique"
a = Point()
a.x = 1
a.y = 2
b = Point()
b.x = 3
b.y = 4
print("a : x =", a.x, "y =", a.y)
print("b : x =", b.x, "y =", b.y)
On a 2 instances de la classe Point
, c’est-à-dire 2 objets de type Point
. Pour chacun d’eux, les attributs prennent des valeurs qui sont propres à l’instance.
Distinction entre variable et objet
L’exemple suivant montre bien la distinction entre variable et objet :
class Point:
"Definition d'un point geometrique"
a = Point()
a.x = 1
a.y = 2
b = a
print("a : x =", a.x, "y =", a.y)
print("b : x =", b.x, "y =", b.y)
a.x = 3
a.y = 4
print("a : x =", a.x, "y =", a.y)
print("b : x =", b.x, "y =", b.y)
Ici les variables a
et b
font référence au même objet. En effet, lors de l’affectation b = a
, on met dans la variable b
la référence contenue dans la variable a
. Par conséquent, toute modification des valeurs des attributs de l’objet dont la référence est contenue dans a
entraîne une modification pour b
.
Avertissement
Par abus de langage on parlera parfois de l’objet a
alors qu’il s’agira en fait de l’objet auquel a
fait référence.
Définition des méthodes#
class Point:
def deplace(self, dx, dy):
self.x = self.x + dx
self.y = self.y + dy
Cette classe possède une méthode : deplace()
.
Pour définir une méthode, il faut :
indiquer son nom (ici
deplace()
).indiquer les arguments entre des parenthèses. Le premier argument d’une méthode doit être
self
.
Pour accéder aux méthodes d’un objet, on indique :
le nom de la variable qui fait référence à cet objet
un point
le nom de la méthode
a.deplace(3, 5)
Avertissement
Lors de l’appel de la méthode, le paramètre self
n’est pas utilisé et la valeur qu’il prend est la référence à l’objet. Il y a donc toujours un paramètre de moins que lors de la définition de la méthode.
Exemple
class Point:
def deplace(self, dx, dy):
self.x = self.x + dx
self.y = self.y + dy
a = Point()
a.x = 1
a.y = 2
print("a : x =", a.x, "y =", a.y)
a.deplace(3, 5)
print("a : x =", a.x, "y =", a.y)
La notion de constructeur#
Si lors de la création d’un objet nous voulons qu’un certain nombre d’actions soit réalisées (par exemple une initialisation), nous pouvons utiliser un constructeur.
Un constructeur n’est rien d’autre qu’une méthode, sans valeur de retour, qui porte un nom imposé par le langage Python : __init__()
. Ce nom est constitué de init
entouré avant et après par __
(deux fois le symbole underscore _
, qui est le tiret sur la touche 8
). Cette méthode sera appelée lors de la création de l’objet. Le constructeur peut disposer d’un nombre quelconque de paramètres, éventuellement aucun.
Exemple sans paramètre
class Point:
def __init__(self):
self.x = 0
self.y = 0
a = Point()
print("a : x =", a.x, "y =", a.y)
a.x = 1
a.y = 2
print("a : x =", a.x, "y =", a.y)
Dans cet exemple, nous avons pu définir des valeurs par défaut pour les attributs grâce au constructeur.
Exemple avec paramètres
class Point:
def __init__(self, abs, ord):
self.x = abs
self.y = ord
a = Point(1, 2)
print("a : x =", a.x, "y =", a.y)
Autre exemple avec paramètres
Dans l’exemple suivant, on utilise les mêmes noms pour les paramètres du constructeur et les attributs. Ceci ne pose pas de problème car ces variables ne sont pas dans le même espace de noms. Les paramètres du constructeur sont des variables locales, comme c’est habituellement le cas pour une fonction. Les attributs de l’objet sont eux dans l’espace de noms de l’instance. Les attributs se distinguent facilement car ils ont self
devant.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
a = Point(1, 2)
print("a : x =", a.x, "y =", a.y)
Exemple complet
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def deplace(self, dx, dy):
self.x = self.x + dx
self.y = self.y + dy
a = Point(1, 2)
b = Point(3, 4)
print("a : x =", a.x, "y =", a.y)
print("b : x =", b.x, "y =", b.y)
a.deplace(3, 5)
b.deplace(-1, -2)
print("a : x =", a.x, "y =", a.y)
print("b : x =", b.x, "y =", b.y)
Exercice
Modifier le programme de façon à ajouter deux autres objets de type Point
. Ils seront référencés par des variables c
et d
.
Exercice
Définir une classe Point3D
qui sera analogue à la classe Point
mais pour des points dans l’espace à 3 dimensions. Créer deux objets de type Point3D
qui seront référencés par les variables a3D
et b3D
. Initialiser ces points et afficher leurs coordonnées x, y, z.
La notion d’encapsulation#
Le concept d”encapsulation est un concept très utile de la POO. Il permet en particulier d’éviter une modification par erreur des données d’un objet. En effet, il n’est alors pas possible d’agir directement sur les données d’un objet ; il est nécessaire de passer par ses méthodes qui jouent le rôle d’interface obligatoire.
Python ne dispose pas d’un mécanisme d’encapsulation strict comme certains autres langages de programmation (par exemple, private en Java), mais il offre des conventions et des mécanismes pour contrôler l’accès aux attributs et aux méthodes.
Voici comment réaliser de l’encapsulation en Python :
Conventions de nommage pour l’encapsulation
Python utilise des conventions de nommage pour indiquer le niveau d’accessibilité des attributs et des méthodes :
Attributs ou méthodes publics : Ces attributs ou méthodes sont accessibles de l’extérieur de la classe. Aucun underscore n’est utilisé avant leur nom.
class Personne:
def __init__(self, nom):
self.nom = nom # Attribut public
def afficher_nom(self):
return self.nom # Méthode publique
Attributs ou méthodes protégés : Ces attributs ou méthodes sont destinés à être utilisés uniquement à l’intérieur de la classe et de ses classes dérivées. Un underscore unique (_) est utilisé avant leur nom.
class Personne:
def __init__(self, nom):
self._nom = nom # Attribut protégé
def _afficher_nom(self):
return self._nom # Méthode protégée
Attributs ou méthodes privés : Ces attributs ou méthodes ne sont pas censés être accessibles directement en dehors de la classe. Deux underscores (__) sont utilisés avant leur nom.
class Personne:
def __init__(self, nom):
self.__nom = nom # Attribut privé
def __afficher_nom(self):
return self.__nom # Méthode privée
Définition d’attributs privés#
A présent, on réalise la protection des attributs de notre classe Point
grâce à l’utilisation d’attributs privées. Pour avoir des attributs privés, leur nom doit débuter par __
(deux fois le symbole underscore _
, qui est le tiret sur la touche 8
).
class Point:
def __init__(self, x, y):
self.__x = x
self.__y = y
Il n’est alors plus possible de faire appel aux attributs __x
et __y
depuis l’extérieur de la classe Point
.
>>> p = Point(1, 2)
>>> p.__x
Traceback (most recent call last):
File "<pyshell#9>", line 1, in
p.__x
AttributeError: Point instance has no attribute '__x'
Il faut donc disposer de méthodes qui vont permettre par exemple de modifier ou d’afficher les informations associées à ces variables.
class Point:
def __init__(self, x, y):
self.__x = x
self.__y = y
def deplace(self, dx, dy):
self.__x = self.__x + dx
self.__y = self.__y + dy
def affiche(self):
print("abscisse =", self.__x, "ordonnee =", self.__y)
a = Point(2, 4)
a.affiche()
a.deplace(1, 3)
a.affiche()
Exemple d’encapsulation
Voici un exemple pratique d’encapsulation en Python :
class CompteBancaire:
def __init__(self, solde):
self.__solde = solde # Attribut privé
def deposer(self, montant):
if montant > 0:
self.__solde += montant
def retirer(self, montant):
if 0 < montant <= self.__solde:
self.__solde -= montant
def afficher_solde(self):
return self.__solde # Méthode publique pour accéder à l'attribut privé
# Utilisation
compte = CompteBancaire(1000)
compte.deposer(500)
compte.retirer(200)
print(compte.afficher_solde()) # Affiche: 1300
Dans cet exemple, l’attribut __solde est privé et ne peut pas être modifié directement de l’extérieur de la classe. Les méthodes deposer, retirer, et afficher_solde fournissent une interface contrôlée pour accéder et modifier cet attribut.
Accesseurs et mutateurs#
Parmi les différentes méthodes que comporte une classe, on a souvent tendance à distinguer :
les accesseurs (en anglais accessors ou getters) qui fournissent des informations relatives à l’état d’un objet, c’est-à-dire aux valeurs de certains de ses attributs (généralement privés) sans les modifier ;
les mutateurs (en anglais mutators ou setter) qui modifient l’état d’un objet, donc les valeurs de certains de ses attributs.
1. Utilisation des accesseurs et mutateurs traditionnels#
Voici un exemple traditionnel d’utilisation des accesseurs et des mutateurs en Python. En effet, on rencontre souvent l’usage de noms de la forme get_xxx()
pour les accesseurs et set_xxx()
pour les mutateurs, y compris dans des programmes dans lesquels les noms de variable sont francisés.
Par exemple, pour la classe Point
sur laquelle nous avons déjà travaillé, on peut définir les méthodes suivantes :
Exemple
class Point:
def __init__(self, x, y):
self.__x = x
self.__y = y
def get_x(self):
return self.__x
def set_x(self, x):
self.__x = x
def get_y(self):
return self.__y
def set_y(self, y):
self.__y = y
a = Point(3, 7)
print("a : abscisse =", a.get_x())
print("a : ordonnee =", a.get_y())
a.set_x(6)
a.set_y(10)
print("a : abscisse =", a.get_x())
print("a : ordonnee =", a.get_y())
L’utilisation d’un mutateur pour fixer la valeur d’un attribut autorise la possibilité d’effectuer un contrôle sur les valeurs de l’attribut. Par exemple, il serait possible de n’autoriser que des valeurs positives pour les attributs privés __x
et __y
.
Notez qu’il n’est pas toujours prudent de prévoir une méthode d’accès pour chacun des attributs privés d’un objet.
Autre exemple
class CompteBancaire:
def __init__(self, solde):
self.__solde = solde # Attribut privé
# Accesseur
def get_solde(self):
return self.__solde
# Mutateur
def set_solde(self, montant):
if montant >= 0:
self.__solde = montant
else:
print("Le solde ne peut pas être négatif.")
# Utilisation
compte = CompteBancaire(1000)
print(compte.get_solde()) # Affiche: 1000
compte.set_solde(1500)
print(compte.get_solde()) # Affiche: 1500
Dans cet exemple :
La méthode get_solde est un accesseur qui retourne la valeur de l’attribut privé __solde.
La méthode set_solde est un mutateur qui permet de modifier la valeur de __solde, en appliquant une validation (par exemple, vérifier que le montant n’est pas négatif).
2. Utilisation des propriétés (@property)#
Python propose une façon plus élégante de définir des accesseurs et des mutateurs à l’aide du décorateur @property. Cela permet de définir des méthodes qui agissent comme des attributs, rendant le code plus simple et plus lisible.
Exemple
class Point:
def __init__(self, x, y):
self.__x = x
self.__y = y
@property
def x(self):
return self.__x
@x.setter
def x(self, x):
self.__x = x
@property
def y(self):
return self.__y
@y.setter
def y(self, y):
self.__y = y
a = Point(3, 7)
print("a : abscisse =", a.x)
print("a : ordonnee =", a.y)
a.x = 6
a.y = 10
print("a : abscisse =", a.x)
print("a : ordonnee =", a.y)
Autre exemple
class CompteBancaire:
def __init__(self, solde):
self.__solde = solde # Attribut privé
@property
def solde(self):
"""Accesseur pour l'attribut solde"""
return self.__solde
@solde.setter
def solde(self, montant):
"""Mutateur pour l'attribut solde"""
if montant >= 0:
self.__solde = montant
else:
print("Le solde ne peut pas être négatif.")
# Utilisation
compte = CompteBancaire(1000)
print(compte.solde) # Utilise l'accesseur, affiche: 1000
compte.solde = 1500 # Utilise le mutateur
print(compte.solde) # Affiche: 1500
compte.solde = -500 # Affiche: Le solde ne peut pas être négatif.
Dans cet exemple :
Le décorateur @property définit une méthode solde qui sert d’accesseur.
Le décorateur @solde.setter définit une méthode qui sert de mutateur pour solde.
A noter : il n’est possible de définir un mutateur @solde.setter que si un accesseur solde a été défini auparavant avec le décorateur @property.
Avantages de l’utilisation de @property#
Vous pouvez accéder aux attributs et les modifier comme s’il s’agissait d’attributs publics ordinaires (compte.solde), mais en réalité, vous utilisez des méthodes d’accès contrôlé.
Vous pouvez modifier l’implémentation interne d’une classe sans casser le code existant qui utilise cette classe. En d’autres termes, les changements que vous apportez à votre classe ne nécessitent pas de modifications dans le code des utilisateurs de cette classe.
Attributs et méthodes de classe#
Attributs de classe#
Exemple :
class A:
nb = 0
def __init__(self, x):
print("creation objet de type A")
self.x = x
A.nb = A.nb + 1
print("A : nb = ", A.nb)
print("Partie 1")
a = A(3)
print("A : nb = ", A.nb)
print("a : x = ", a.x, " nb = ", a.nb)
print("Partie 2")
b = A(6)
print("A : nb = ", A.nb)
print("a : x = ", a.x, " nb = ", a.nb)
print("b : x = ", b.x, " nb = ", b.nb)
c = A(8)
print("Partie 3")
print("A : nb = ", A.nb)
print("a : x = ", a.x, " nb = ", a.nb)
print("b : x = ", b.x, " nb = ", b.nb)
print("c : x = ", c.x, " nb = ", c.nb)
Méthodes de classe#
Exemple :
class A:
nb = 0
def __init__(self):
print("creation objet de type A")
A.nb = A.nb + 1
print("il y en a maintenant ", A.nb)
@classmethod
def get_nb(cls):
return A.nb
print("Partie 1 : nb objets = ", A.get_nb())
a = A()
print("Partie 2 : nb objets = ", A.get_nb())
b = A()
print("Partie 3 : nb objets = ", A.get_nb())
Pour créer une méthode de classe, il faut la faire précéder d’un « décorateur » : @classmethod
Le premier argument de la méthode de classe doit être cls
.