L'encapsulation en Python
Apprenez l'encapsulation Python : membres publics, protégés et privés, la déformation de noms et les getters/setters avec @property.
L'encapsulation est l'un des quatre piliers de la programmation orientée objet. Elle consiste à regrouper les données d'un objet (attributs) et les méthodes qui opèrent sur ces données dans une seule unité, tout en contrôlant les parties de l'objet auxquelles le monde extérieur peut accéder ou qu'il peut modifier.
Bien appliquée, l'encapsulation maintient la cohérence de l'état interne d'un objet, masque les détails d'implémentation afin de pouvoir les modifier ultérieurement sans casser les appelants, et rend vos classes plus sûres à utiliser.
Ce chapitre couvre :
- Ce qu'est l'encapsulation et pourquoi elle est importante
- Les membres publics, protégés et privés — et les conventions de nommage utilisées par Python
- La déformation de noms — comment fonctionnent réellement les attributs
__double_underscore - Les getters et setters avec
@property - Un exemple concret qui rassemble tout
Avant de lire, assurez-vous d'être à l'aise avec les classes et objets Python. Pour le contrôle d'accès via des attributs calculés, consultez le chapitre étroitement lié sur @property.
Pourquoi l'encapsulation est importante
Considérez un compte bancaire. En interne, il suit un solde. Si ce solde était un attribut ordinaire que n'importe qui pouvait définir, rien n'empêcherait un bug (ou un acteur malveillant) de faire :
account.balance = -9999999L'encapsulation résout ce problème en cachant le solde derrière une interface contrôlée. Le code externe à la classe ne peut effectuer des dépôts ou des retraits qu'à travers des méthodes qui appliquent les règles métier. Le stockage interne est un détail d'implémentation — les appelants ne le touchent jamais directement.
Les trois avantages que cela vous procure sont :
- Intégrité des données — la logique de validation en un seul endroit, appliquée à chaque fois.
- Flexibilité — vous pouvez changer la représentation interne (par exemple, stocker les soldes en centimes plutôt qu'en euros) sans toucher au code appelant.
- Couplage réduit — les appelants dépendent uniquement de l'interface publique, et non du fonctionnement interne de la classe.
Niveaux d'accès : public, protégé et privé
Python ne possède pas de modificateurs d'accès comme les mots-clés private ou public. À la place, il utilise une convention de nommage pour signaler l'intention :
| Préfixe | Exemple | Niveau d'accès | Signification |
|---|---|---|---|
| Aucun préfixe | balance | Public | Destiné à être utilisé par n'importe qui |
Underscore simple _ | _balance | Protégé | Pour un usage interne et les sous-classes ; utilisable en externe mais déconseillé |
Double underscore __ | __pin | Privé | Pour cette classe uniquement ; Python le renomme activement pour empêcher un accès facile |
Ce sont des conventions et mécanismes, pas des règles strictes appliquées par un compilateur. Python fait confiance aux développeurs pour respecter ce signal.
Membres publics
Les attributs et méthodes publics forment l'interface officielle de la classe — la partie que vous avez l'intention de faire utiliser par les appelants :
class BankAccount:
account_type = 'savings' # public class attribute
def __init__(self, owner, balance):
self.owner = owner # public instance attribute
def deposit(self, amount):
pass # public methodAucun nommage spécial n'est nécessaire. N'importe quel code peut lire ou écrire librement un membre public.
Membres protégés (underscore simple _)
Un underscore en tête est un signal qui dit « ceci est un détail interne — ne vous y fiez pas depuis l'extérieur de la classe ». Python n'applique pas cela ; c'est purement une convention :
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance # protected — internal, but subclasses may need it
def _validate_amount(self, amount): # protected helper
return isinstance(amount, (int, float)) and amount > 0_balance reste accessible comme account._balance depuis l'extérieur, mais l'underscore avertit les autres développeurs (et les linters) qu'ils enfreignent le contrat prévu.
Utilisation courante : une classe de base stocke des données dans un attribut _ pour que les sous-classes puissent le lire, tout en le gardant caché du code non lié.
Membres privés (double underscore __)
Un double underscore en tête déclenche la déformation de noms — Python renomme l'attribut en interne en _ClassName__attribute. Cela rend l'accès accidentel depuis l'extérieur beaucoup plus difficile :
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance
self.__pin = 1234 # private — not meant to be touched at all
def verify_pin(self, pin):
return pin == self.__pinDepuis l'extérieur de la classe :
acc = BankAccount('Alice', 1000)
print(acc.owner) # Alice — public, fine
print(acc._balance) # 1000 — protected, works but frowned upon
print(acc.__pin) # AttributeError: 'BankAccount' object has no attribute '__pin'L'attribut existe toujours, mais sous un nom différent. Consultez la section suivante pour savoir comment le trouver.
La déformation de noms
Lorsque Python voit self.__name dans une définition de classe, il le réécrit en interne en self._ClassName__name. C'est la déformation de noms. Le but est d'éviter les conflits accidentels dans les sous-classes — pas de fournir une vraie sécurité.
class Counter:
def __init__(self):
self.__count = 0
def increment(self):
self.__count += 1
def value(self):
return self.__count
c = Counter()
c.increment()
c.increment()
print(c.value()) # 2
# Direct access fails:
# print(c.__count) # AttributeError
# But mangled name still works if you know it:
print(c._Counter__count) # 2Vous pouvez inspecter tous les attributs avec vars() ou dir() pour découvrir le nom déformé :
print(list(vars(c)))
# ['_Counter__count']Déformation de noms et héritage
La déformation de noms est particulièrement utile dans l'héritage. Sans elle, une sous-classe pourrait accidentellement écraser un attribut privé de son parent en utilisant le même nom. Avec la déformation, chaque classe dispose de son propre espace de noms :
class Base:
def __init__(self):
self.__secret = 'base'
def reveal(self):
return self.__secret # accesses _Base__secret
class Child(Base):
def __init__(self):
super().__init__()
self.__secret = 'child' # stored as _Child__secret, not the same thing
def reveal_child(self):
return self.__secret # accesses _Child__secret
c = Child()
print(c.reveal()) # base — Base.reveal() reads _Base__secret
print(c.reveal_child()) # child — Child.reveal_child() reads _Child__secretLes deux attributs coexistent sans collision, ce qui ne serait pas possible sans la déformation de noms.
Getters et setters avec @property
Dans de nombreux langages, vous écrivez des méthodes explicites get_x() et set_x(). Python offre une approche plus propre : le décorateur @property vous permet d'exposer une méthode comme si c'était un attribut ordinaire, de sorte que le code appelant reste lisible tout en gardant un contrôle total sur la lecture et l'écriture.
Getter de base
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsiusLes appelants lisent t.celsius, pas t.celsius(). Le @property rend l'appel de méthode invisible :
t = Temperature(25)
print(t.celsius) # 25 — no parentheses neededAjouter un setter avec validation
Associez @property à un .setter pour valider les valeurs avant de les stocker :
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('Temperature below absolute zero')
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32t = Temperature(25)
print(t.celsius) # 25
print(t.fahrenheit) # 77.0
t.celsius = 100
print(t.fahrenheit) # 212.0
t.celsius = -300 # ValueError: Temperature below absolute zerofahrenheit est une propriété calculée en lecture seule — aucun setter n'est défini, donc Python lève une AttributeError si vous essayez de lui assigner une valeur.
Pourquoi préférer @property aux getters/setters ordinaires ?
Vous pouvez commencer avec un attribut public ordinaire et le convertir en propriété plus tard sans changer le code appelant :
# v1 — plain attribute
class Circle:
def __init__(self, radius):
self.radius = radius
# v2 — property with validation, same public interface
class Circle:
def __init__(self, radius):
self.radius = radius # still works from the caller's point of view
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError('Radius cannot be negative')
self._radius = valueLes appelants qui ont écrit c.radius = 5 continuent de fonctionner sans changement. Seul le comportement change — vous validez désormais la valeur.
Pour une référence complète sur @property incluant les deleters, consultez Python @property.
Un exemple complet : compte utilisateur
L'exemple suivant montre les trois niveaux d'accès fonctionnant ensemble dans une classe réaliste :
class UserAccount:
def __init__(self, username, password):
self.username = username # public
self._login_attempts = 0 # protected — subclasses may need this
self.__password_hash = self.__hash(password) # private
def __hash(self, password):
"""Private helper — implementation detail, may change."""
return hash(password)
def check_password(self, password):
"""Public method — part of the official interface."""
return self.__hash(password) == self.__password_hash
def login(self, password):
if self._login_attempts >= 3:
return 'Account locked'
if self.check_password(password):
self._login_attempts = 0
return 'Login successful'
self._login_attempts += 1
return f'Wrong password ({self._login_attempts}/3)'
user = UserAccount('alice', 'secret123')
print(user.login('bad')) # Wrong password (1/3)
print(user.login('bad')) # Wrong password (2/3)
print(user.login('bad')) # Wrong password (3/3)
print(user.login('secret123')) # Account lockedÀ noter :
usernameest public — il est tout à fait acceptable pour n'importe qui de le lire._login_attemptsest protégé — une sous-classeThrottledAccountpourrait le lire pour implémenter une logique plus intelligente.__password_hashet__hash()sont privés — la stratégie de stockage du mot de passe est purement interne. Les appelants n'ont aucune raison de la voir, et si vous passez plus tard à bcrypt, vous ne changez que ces deux éléments.
Encapsulation et les autres piliers de la POO
L'encapsulation est l'un des quatre principes de la POO :
| Principe | Définition en une ligne |
|---|---|
| Encapsulation | Regrouper données + méthodes ; cacher les détails internes |
| Héritage | Permettre à une classe de réutiliser et d'étendre une autre classe |
| Polymorphisme | Permettre à différents types de répondre au même appel de méthode |
| Abstraction | Exposer une interface simplifiée ; cacher la complexité |
Consultez l'héritage Python, le polymorphisme Python et les classes abstraites Python pour les autres piliers.
Référence rapide
| Convention | Ce qu'elle signale | Appliquée par Python ? |
|---|---|---|
name | Public — utilisation libre | Non (toujours accessible) |
_name | Protégé — usage interne | Non (accessible mais conventionnellement hors limites) |
__name | Privé — cette classe uniquement | Partiellement — le nom est déformé en _ClassName__name |
@property | Accès en lecture contrôlé | Oui — hooks getter/setter/deleter |
@name.setter | Accès en écriture contrôlé avec validation | Oui |