W3docs

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 = -9999999

L'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 :

  1. Intégrité des données — la logique de validation en un seul endroit, appliquée à chaque fois.
  2. 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.
  3. 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éfixeExempleNiveau d'accèsSignification
Aucun préfixebalancePublicDestiné à être utilisé par n'importe qui
Underscore simple __balanceProtégéPour un usage interne et les sous-classes ; utilisable en externe mais déconseillé
Double underscore ____pinPrivé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 method

Aucun 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.__pin

Depuis 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)  # 2

Vous 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__secret

Les 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._celsius

Les 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 needed

Ajouter 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 + 32
t = 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 zero

fahrenheit 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 = value

Les 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 :

  • username est public — il est tout à fait acceptable pour n'importe qui de le lire.
  • _login_attempts est protégé — une sous-classe ThrottledAccount pourrait le lire pour implémenter une logique plus intelligente.
  • __password_hash et __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 :

PrincipeDéfinition en une ligne
EncapsulationRegrouper données + méthodes ; cacher les détails internes
HéritagePermettre à une classe de réutiliser et d'étendre une autre classe
PolymorphismePermettre à différents types de répondre au même appel de méthode
AbstractionExposer 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

ConventionCe qu'elle signaleAppliquée par Python ?
namePublic — utilisation libreNon (toujours accessible)
_nameProtégé — usage interneNon (accessible mais conventionnellement hors limites)
__namePrivé — cette classe uniquementPartiellement — le nom est déformé en _ClassName__name
@propertyAccès en lecture contrôléOui — hooks getter/setter/deleter
@name.setterAccès en écriture contrôlé avec validationOui

Pratique

Pratique
Que signale un underscore en tête (ex. `_balance`) en Python ?
Que signale un underscore en tête (ex. `_balance`) en Python ?
Was this page helpful?