Mécanismes d'encapsulation et de masquage
L'encapsulation est un concept fondamental de la programmation orientée objet (POO) qui vise à regrouper les données (attributs) et les comportements (méthodes) au sein d'une même entité, tout en contrôlant l'accès aux éléments internes. En Python, ce contrôle s'effectue principalement par le masquage des attributs pour empêcher les modifications directes et non autorisées depuis l'extérieur de la classe.
L'un des moyens de réaliser ce masquage est l'utilisation du "name mangling" (ou torsion de nom). En préfixant un nom d'attribut par deux tirets bas (__), Python modifie son nom interne pour le rendre inaccessible directement via son nom original.
class SystemConfig:
__cle_secrete = "A1B2-C3D4"
# Tenter d'accéder directement provoque une erreur
print(SystemConfig.__cle_secrete) # AttributeError
Lorsqu'on inspecte l'espace de noms de la classe avec __dict__, on constate que l'attribut __cle_secrete a été renommé en _SystemConfig__cle_secrete. Ce mécanisme permet de protéger la variable contre les écrasements accidentels, bien qu'elle reste techniquement accessbile via ce nom transformé.
print(SystemConfig.__dict__)
# Affiche : {'_SystemConfig__cle_secret': 'A1B2-C3D4', ...}
Cette technique est particulièrement utile dans la méthode constructeur __init__ pour sécuriser les données spécifiques à chaque instance.
Gestion de l'accès avec @property
Bien que le masquage protège les données, il est souvent nécessaire de fournir un accès contrôlé, en lecture seule ou en lecture-écriture, sans exposer la structure interne. Python utilise des décorateurs pour transformer des méthodes en attributs accessibles, offrant ainsi une interface propre et pythonique.
L'usage de @property permet de définir un accesseur (getter) qui est appelé comme un simple attribut.
class CompteBancaire:
def __init__(self, titulaire, solde_initial):
self.__titulaire = titulaire
self.__solde = solde_initial
@property
def solde(self):
"""Retourne le solde sans permettre la modification directe"""
return self.__solde
mon_compte = CompteBancaire("Alice", 1000)
print(mon_compte.solde) # 1000
# La ligne suivante lèverait une erreur car le setter n'est pas défini
# mon_compte.solde = 5000 # AttributeError: can't set attribute
Pour permettre la modification ou la suppression de ces données "privées", on peut ajouter des décorateurs @setter et @deleter. Cela permet de valider les données avant de les mettre à jour.
class CompteBancaire:
def __init__(self, titulaire, solde_initial):
self.__titulaire = titulaire
self.__solde = solde_initial
@property
def solde(self):
return self.__solde
@solde.setter
def solde(self, nouveau_montant):
if nouveau_montant < 0:
raise ValueError("Le solde ne peut pas être négatif")
self.__solde = nouveau_montant
@solde.deleter
def solde(self):
print("Suppression de la donnée solde...")
del self.__solde
mon_compte = CompteBancaire("Alice", 1000)
mon_compte.solde = 2000 # Modification autorisée et validée
print(mon_compte.solde)
del mon_compte.solde # Suppression via la méthode
Polymorphisme et typage canard (Duck Typing)
Le polymorphisme en Python se manifeste par la capacité de différents objets à répondre à la même interface ou méthode, indépendamment de leur hiérarchie de classes. Python privilégie le "Typage Canard" : si un objet marche comme un canard et cancane comme un canard, alors on peut le traiter comme un canard. L'important est le comportement (les méthodes disponibles) et non le type strict de l'objet.
Prenons l'exemple de différentes structures de données qui partagent une capacité commune, comme l'itération ou le calcul de taille, ou des classes personnalisées implémentant une méthode executer.
class Chien:
def emettre_son(self):
return "Aboie"
class Chat:
def emettre_son(self):
return "Miaule"
class Voiture:
def emettre_son(self):
return "Vroum"
def animer(entite):
print(f"Cette entite fait : {entite.emettre_son()}")
animer(Chien()) # Cette entite fait : Aboie
animer(Voiture()) # Cette entite fait : Vroum
Dans cet exemple, la fonction animer ne se soucie pas de la classe de l'objet, tant que la méthode emettre_son existe.
Classes abstraites et contrats d'interface
Bien que le typage canard offre une grande flexibilité, il est parfois nécessaire d'imposer que toutes les sous-classes implémentent certaines méthodes. C'est ici qu'interviennent les classes abstraites, définies via le module abc (Abstract Base Classes). Une classe abstraite ne peut pas être instanciée directement et sert de modèle.
En utilisant le décorateur @abc.abstractmethod, on force les classes filles à redéfinir les méthodes spécifiées, sous peine de lever une erreur lors de l'instanciation.
import abc
class ConnecteurBase(abc.ABC):
@abc.abstractmethod
def connecter(self):
pass
@abc.abstractmethod
def deconnecter(self):
pass
class ServiceBDD(ConnecteurBase):
def connecter(self):
print("Connexion à la base de données établie.")
# La méthode deconnecter est oubliée ici
# L'instanciation échouera car ServiceBDD n'est pas une implémentation concrète complète
# srv = ServiceBDD() # TypeError: Can't instantiate abstract class ServiceBDD with abstract method deconnecter