L'encapsulation en programmation orientée objet consiste à regrouper les données et les méthodes qui les manipulent au sein d'une même entité, tout en contrôlant l'accès externe. Cela va au-delà de la simple dissimulation des détails internes.
Dissimulation des attributs en Python
En Python, on peut rendre des attributs privés en les préfixant avec deux undersocres. Ce mécanisme applique une transformation de noms lors de la définition de la classe, où les noms comme __attribut deviennent _NomClasse__attribut.
class CompteBancaire:
__solde = 0
def __init__(self, titulaire):
self.titulaire = titulaire
self.__fonds = 1000 # Transformé en _CompteBancaire__fonds
def _calculer_frais(self): # Transformé en _CompteBancaire__calculer_frais
print('Calcul des frais')
def afficher_solde(self):
self.__fonds = self.__solde + 100 # Accès interne uniquement
# Les attributs transformés peuvent être accédés de l'extérieur avec le nom transformé
# instance._CompteBancaire__fonds est valide, mais cela contourne l'encapsulation.
Points importants à noter :
- Cette transformation ne bloque pas véritablement l'accès externe ; elle le rend seulement plus difficile.
- La transformation se produit une seule fois lors de la définition de la classe, pas lors des affectations ultérieures.
- Dans l'héritage, une méthode privée empêche la substitution par les classes dérivées.
class A:
def __methode_privee(self):
print('Méthode de A')
def executer(self):
self.__methode_privee()
class B(A):
def __methode_privee(self):
print('Méthode de B')
instance = B()
instance.executer() # Appelle _A__methode_privee, affichant 'Méthode de A'
Encapsulation au-delà de la dissimulation
L'objectif principal de l'encapsulation est de définir des interfaces claires pour interagir avec les attributs cachés. Cela permet d'ajouter des contrôles ou de simplifier l'utilisation.
Exemple de données encapsulées avec validation :
class Employe:
def __init__(self, nom, age):
self.definir_infos(nom, age)
def afficher_infos(self):
print(f'Nom : {self._Employe__nom}, Âge : {self._Employe__age}')
def definir_infos(self, nom, age):
if not isinstance(nom, str):
raise TypeError('Le nom doit être une chaîne')
if not isinstance(age, int):
raise TypeError('L\'âge doit être un entier')
self.__nom = nom
self.__age = age
employe = Employe('Marie', 30)
employe.afficher_infos()
employe.definir_infos('Marie', 31)
Exemple de méthodes encapsulées pour isoler la complexité :
class DistributeurATM:
def __inserer_carte(self):
print('Insertion de la carte')
def __authentifier(self):
print('Authentification')
def __saisir_montant(self):
print('Saisie du montant')
def __imprimer_recu(self):
print('Impression du reçu')
def __retirer_argent(self):
print('Retrait')
def effectuer_retrait(self):
self.__inserer_carte()
self.__authentifier()
self.__saisir_montant()
self.__imprimer_recu()
self.__retirer_argent()
atm = DistributeurATM()
atm.effectuer_retrait()
En Python, la convention d'attributs avec un seul underscore indique un usage interne, mais ils restent accessibles. Pour un contrôle strict, des méthodes spéciales comme __getattr__ peuvent être utilisées.
Propriétés (property)
Les propriétés permettent d'accéder à des méthodes comme à des attributs, en exécutant du code lors de la lecture ou de l'écriture.
Exemple avec un calcul d'IMC :
class Personne:
def __init__(self, nom, poids, taille):
self.nom = nom
self.poids = poids
self.taille = taille
@property
def imc(self):
return self.poids / (self.taille ** 2)
personne = Personne('Luc', 70, 1.75)
print(personne.imc)
Exemple avec un cercle :
import math
class Cercle:
def __init__(self, rayon):
self.rayon = rayon
@property
def aire(self):
return math.pi * self.rayon ** 2
@property
def perimetre(self):
return 2 * math.pi * self.rayon
cercle = Cercle(5)
print(cercle.aire)
print(cercle.perimetre)
Les propriétés peuvent inclure des setters et des deleteurs pour contrôler les modifications :
class Configuration:
def __init__(self, valeur):
self._valeur = valeur
@property
def parametre(self):
return self._valeur
@parametre.setter
def parametre(self, valeur):
if not isinstance(valeur, str):
raise TypeError('La valeur doit être une chaîne')
self._valeur = valeur
@parametre.deleter
def parametre(self):
raise TypeError('Suppression interdite')
config = Configuration('défaut')
print(config.parametre)
config.parametre = 'nouvelle_valeur'
# del config.parametre lèverait une exception
Encapsulation et extensibilité
L'encapsulation permet de modifier l'implémentation interne sans affecter le code externe, tant que les interfaces restent inchangées.
class Piece:
def __init__(self, nom, proprietaire, largeur, longueur):
self.nom = nom
self.proprietaire = proprietaire
self.__largeur = largeur
self.__longueur = longueur
def calculer_surface(self): # Interface externe
return self.__largeur * self.__longueur
# Utilisation
piece = Piece('Salon', 'Alice', 5, 6)
print(piece.calculer_surface()) # Affiche 30
# Modification interne pour calculer le volume sans changer l'interface
class Piece:
def __init__(self, nom, proprietaire, largeur, longueur, hauteur):
self.nom = nom
self.proprietaire = proprietaire
self.__largeur = largeur
self.__longueur = longueur
self.__hauteur = hauteur
def calculer_surface(self): # Même nom, mais logique modifiée pour le volume
return self.__largeur * self.__longueur * self.__hauteur
piece = Piece('Salon', 'Alice', 5, 6, 3)
print(piece.calculer_surface()) # Affiche 90, mais l'appel reste identique