1.5. Les membres privés¶
L’encapsulation est un des principes fondamentaux des langages de programmation orientés objet. Il spécifie que le fonctionnement interne d’une classe doit être isolé de l’extérieur, de manière à ce que toute interaction avec la classe se fasse indirectement et uniquement via des méthodes prévues à cet effet.
Nous pouvons faire une analogie avec une voiture. Le conducteur peut klaxonner, appuyer sur les pédales et le frein, lire la vitesse et le niveau d’essence sur des compteurs, mais toutes ces actions sont indirectes. Il ne peut ni faire tourner les roues manuellement pour avancer, ni accélérer en modifiant la valeur directement du compteur de vitesse. En programmation, l’encapsulation assure l’intégrité des données internes à la classe sans que l’utilisateur ne puisse les modifier directement.
Écrivons par exemple une classe Sum
qui calcule et stocke dans des attributs la somme d’un ensemble de valeurs passé en paramètre à l’initialisation, ainsi que leur nombre.
Nous définissons également une méthode add()
pour ajouter ultérieurement une valeur à la somme tout en s’assurant que le nombre de valeurs soit mis à jour.
La classe peut ensuite renvoyer le total ou la moyenne de ces valeurs grâce aux méthodes total()
et average()
.
class Sum:
def __init__(self, *values):
total = 0
for v in values:
total += v
self.total = total
self.nb = len(values)
def add(self, value):
self.total += value
self.nb += 1
def get_total(self):
return self.total
def get_average(self):
return self.total / self.nb
s = Sum(4, 17, 3, 56)
print(s.get_total()) # équivalent à `print(s.total)`
print(s.get_average())
s.add(40)
print(s.get_total())
print(s.get_average())
80
20.0
120
24.0
Le programme s’exécute sans fautes, mais notre classe comporte cependant un grave problème de fiabilité.
En effet, l’exactitude du résultat du calcul de la moyenne dépend de la mise à jour du nombre de valeurs, ce dont la méthode add()
est chargée.
Or, il est possible dans un programme utilisant cette classe de modifier la somme sans toucher au compteur de valeurs, en accédant directement à l’attribut total
.
Une telle modification mettrait l’objet dans un état illégal, dans lequel il n’est pas censé se trouver.
Dès lors, le calcul de la moyenne sera systématiquement faux (à moins d’être artificiellement corrigé via une seconde modification directe des attributs), comme dans l’example ci-dessous.
s.total += 4 # modification "interdite" de la somme
print(s.get_average()) # le calcul de la moyenne est erroné
24.8
Le fait que l’utilisateur puisse manipuler directement les attributs d’un objet introduit un risque d’erreur non négligeable, d’autant qu’il n’a même pas besoin de savoir que les attributs total
et nb
existent.
En effet, une autre implémentation de cette classe pourrait, par exemple, garder en mémoire la liste entière de toutes les valeurs à la place de ces deux attributs.
Ce sont des variables internes à la classe qui assurent son bon fonctionnement et l’utilisateur, en y accédant directement, risque que de perturber ce comportement.
Il faudrait donc pouvoir protéger ces attributs de toute interférence venant de l’extérieur.
En programmation orientée objet, on distingue en général trois niveaux de protection pour les membres (attributs et méthodes) des classes.
Niveaux de protection des membres d’une classe
Niveau |
Description |
---|---|
public |
accessible sans restriction depuis l’extérieur de la classe (niveau par défaut) |
protégé |
accessible uniquement depuis l’intérieur de la classe et des classes filles |
privé |
accessible uniquement depuis l’intérieur de la classe |
Python n’étant pas purement un langage à objet, il n’offre malheureusement aucun mot-clé permettant de déclarer et de faire respecter le niveau de protection des membres d’une classe (qui sont tous publics par défaut).
Cependant, la convention veut que le nom des membres que l’on souhaite déclarer comme protégés commence par un symbole _
(tiret bas) et que celui des membres privés commence par deux symboles _
(ne pas confondre avec les méthodes spéciales, qui commencent et se terminent aussi par __
).
Libre ensuite à l’utilisateur de suivre ces indications ou non, mais il ne peut pas les ignorer.
De plus, cette convention exploite un mécanisme de Python appelé « name mangling » qui oblige à ajouter le nom de la classe précédé d’un symbole _
avant celui des membres dont le nom commence par __
s’il veut vraiment y accéder depuis l’extérieur, de la manière suivante : _ClassName__private_member_name
.
Toute tentative d’accéder aux membres privés de la classe sans respecter cette syntaxe provoque un erreur !
Nous pouvons maintenant réécrire la classe Sum
et observer la différence.
class Sum:
def __init__(self, *values):
total = 0
for v in values:
total += v
self.__total = total
self.__nb = len(values)
def add(self, value):
self.__total += value
self.__nb += 1
def get_total(self):
return self.__total
def get_average(self):
return self.__total / self.__nb
s = Sum(4, 17, 3, 56)
print(s.get_total()) # accès correct
print(s.get_average())
# accès direct à la somme (en principe interdit mais possible)
print(s._Sum__total)
s.__total += 4 # erreur !
80
20.0
80
Traceback (most recent call last):
File "<input>", line...
s.__total += 4
AttributeError: 'Sum' object has no attribute '__total'
Grâce à l’encapsulation, on peut modifier la mise en œuvre interne de notre classe sans impacter le code qui l’utilise (via ses méthodes publiques). Notre classe pourrait, par exemple, stocker l’ensemble des valeurs dans une liste et calculer le total et la moyenne à la demande, comme illustré ci-dessous. Le résultat produit sera identique.
class Sum:
def __init__(self, *values):
self.__values = list(values)
def add(self, value):
self.__values.append(value)
def get_total(self):
total = 0
for v in self.__values:
total += v
return total
def get_average(self):
return self.get_total() / len(self.__values)
s = Sum(4, 17, 3, 56)
print(s.get_total())
print(s.get_average())
Si une méthode est prévue pour n’être utilisée qu’à l’intérieur d’une classe, elle peut de la même manière être déclarée protégée ou privée en précédant son nom du nombre approprié de symboles _
.
Note
Bien qu’elle n’est pas nativement supportée par le language, l’encapsulation en Python est utile dans de nombreuses situations et sa pratique est encouragée. Il est toutefois largement accepté de ne pas utiliser les conventions de nommage pour restreindre l’accès aux attributs privés ou protégés lorsque cela ne nuit pas au bon fonctionnement de la classe.