Apprenez en lisant dans l'ordre

Variables privées et encapsulation — Accès sûr via getter / setter

Apprends les variables privées et l'encapsulation en Python. La convention _x vs le name mangling __x, get_xxx / set_xxx pour un accès sûr, et le style Pythonique @property / @xxx.setter — tout en pratique.

La dernière fois on a couvert les deux premiers piliers de la POO — héritage et polymorphisme. Cet article boucle le troisième : l'encapsulation.

Variables privées — Python n'a pas de vraie confidentialité

Java et C++ ont un mot-clé private qui bloque l'accès depuis l'extérieur dès que tu le déclares. Python n'a pas de confidentialité imposée par le langage. À la place, le nombre de underscores en tête signale « c'est pour usage interne » ou « ne touche pas directement à ça » — une convention entre programmeurs, pas une règle stricte.

0 / 1 / 2 underscores transmettent l'intention
name(aucun)attr publiclibre d'usagedepuis l'extérieur_name(1)convention :privéne pas toucher__name(2)
Le nombre d'underscores ne change pas l'application ; c'est juste une étiquette qui signale l'intention.

Underscore simple _x — confidentialité par convention seulement

Ajouter un seul _ devant un nom dit à la communauté Python « cet attribut est pour l'usage interne de la classe — n'y accède pas directement depuis l'extérieur ». Le paramètre de __init__ garde son nom simple ; seul le champ self. reçoit le _ en tête.

class UserAccount:
    def __init__(self, owner_name, balance):
        self._owner_name = owner_name      # interne -> préfixer avec _
        self._balance    = balance

    def get_info(self):                     # accesseur exposé
        return {"owner": self._owner_name, "balance": self._balance}


user = UserAccount("Alice", 50000)
print(user._balance)        # 50000  <- marche mais déconseillé
print(user.get_info())      # {'owner': 'Alice', 'balance': 50000}  <- recommandé

Utilise une classe BlogPost pour ressentir ce que _ veut dire (scénario différent du sample ci-dessus — même structure, champs différents).

① Définis class BlogPost: et affecte self._title et self._views dans __init__(self, title, views).

② Définis summary(self) pour return {"title": self._title, "views": self._views}.

③ Crée post = BlogPost("Débuter avec Python", 100). D'abord, accède directement avec post._views et print le résultat (ça marche mais c'est un pattern interdit).

④ Ensuite utilise le pattern recommandéprint(post.summary()).

(Une fois que ça tourne correctement, l'explication apparaîtra.)

Éditeur Python

Exécuter le code pour voir le résultat

Underscore double __x — name mangling

Ajouter deux _ en tête fait réécrire le nom de l'attribut lui-même par Python. Si tu écris self.__pin = 1234 dans une classe Account, le nom réellement stocké devient _Account__pin. C'est ce qu'on appelle le name manglingobj.__pin depuis l'extérieur ne trouve rien, donc l'accès devient effectivement bien plus difficile.

__pin est réécrit en _Account__pin en interne
self.__pin= 1234Python réécritle nom_Account__pin= 1234obj.__pin-> Errorobj._Account__pin-> 1234stocke
Les attributs en double underscore sont stockés sous « underscore + nom de classe + nom original ». Lire obj.__pin directement échoue avec AttributeError parce que cette clé n'est pas là.
class Account:
    def __init__(self, owner, pin):
        self._owner = owner       # confidentialité par convention seulement
        self.__pin  = pin         # name-mangled (devient _Account__pin)

acc = Account("Alice", 1234)

print(acc._owner)              # Alice            <- marche normalement
# print(acc.__pin)             # AttributeError <- non visible directement
print(acc._Account__pin)       # 1234            <- le nom mangled y accède

Utilise une classe LoginForm pour vérifier que __password est vraiment réécrit (scénario différent du sample Account, même comportement).

① Définis class LoginForm: et dans __init__(self, username, password) affecte self._username = username et self.__password = password.

② Crée form = LoginForm("alice", "p@ssw0rd"). Utilise print(form._username) pour confirmer que le côté underscore simple se lit normalement.

③ Utilise print(form._LoginForm__password) pour récupérer le mot de passe via son nom mangled.

④ Lance print([n for n in dir(form) if not n.startswith('__')]) pour exposer les noms d'attributs réellement stockés sur l'instance et voir _LoginForm__password listé.

Éditeur Python

Exécuter le code pour voir le résultat

« __ » n'est pas un mur absolu non plus

Le double underscore empêche l'accès direct via obj.__pin, ce qui est un cran plus fort. Mais quiconque connaît le nom mangled obj._Account__pin peut toujours y accéder. Ce n'est pas une vraie confidentialité. Dans les vrais projets, l'underscore simple _x est bien plus courant sauf raison spécifique d'utiliser le mangling.

Encapsulation — restreindre l'accès à des méthodes dédiées

L'encapsulation est l'idée de design « regrouper les attributs de données et les méthodes qui agissent dessus dans une seule classe, et forcer le code extérieur à passer par un petit ensemble de portes d'entrée publiées ». Alors comment construit-on ces portes d'entrée ?

Canaliser lectures/écritures dans une seule méthode
cohérencebriséeécriture directe_price = -100❌ extérieurproduct._price= -100cohérencepréservéeset_price()valide✅ extérieurproduct.set_price(100)tel quelseulement si OK
Les écritures directes laissent n'importe quelle valeur atterrir dans _price sans contrôle. Passer par une méthode signifie que le setter valide le type et la plage en un seul endroit.

Le style le plus basique consiste à écrire les méthodes get_xxx / set_xxx à la main. Dans le setter, fais une vérification isinstance pour le type et un contrôle de plage, et raise ValueError(...) si quelque chose cloche. Avec ça en place, aucune valeur poubelle n'atteint jamais _price.

class Product:
    def __init__(self, name, price, stock):
        self._name  = name
        self._price = price
        self._stock = stock

    def get_price(self):
        return self._price

    def set_price(self, price):
        if isinstance(price, int) and price >= 0:
            self._price = price
        else:
            raise ValueError("price must be a non-negative integer")


product = Product("T-shirt", 1500, 30)
print(product.get_price())     # 1500
product.set_price(2000)
print(product.get_price())     # 2000
# product.set_price(-100)      # ValueError

Ajoute un getter et setter dédiés à l'âge à une classe de formulaire d'inscription UserProfile. L'âge ne peut pas être négatif ni atteindre 200, donc le setter doit garder la plage.

① Définis class UserProfile: et dans __init__(self, name, age) affecte _name et _age.

② Définis get_age(self) et return self._age.

③ Définis set_age(self, age). Seulement quand age est int et 0 <= age <= 150 il doit exécuter self._age = age ; sinon raise ValueError("age must be an integer in [0, 150]").

④ Crée user = UserProfile("Alice", 30), print user.get_age(), puis user.set_age(31) et print à nouveau le résultat.

⑤ Confirme le chemin de rejet : enveloppe user.set_age(-5) dans try / except ValueError as e: et print("NG:", e).

Éditeur Python

Exécuter le code pour voir le résultat

@property et @xxx.setter

Le style get_price() / set_price(...) est clair, mais les sites d'appel finissent par avoir l'air très méthode-call — pas le plus propre. L'idiome Python plus poli utilise deux décorateurs : @property et @xxx.setter.

Avec ceux-là, le site d'appel reste product.price / product.price = 2000accès d'attribut classique — mais en dessous, les méthodes getter et setter sont appelées. C'est une structure à deux couches où la syntaxe reste simple mais la logique tourne quand même.

product.price a la même apparence ; les méthodes tournent en dessous
product.price@propertydef pricereturnself._priceproduct.price= 2000@price.setterdef pricevalide, puismaj self._pricelitécrit
La syntaxe d'accès d'attribut reste telle quelle ; @property redirige les lectures et @price.setter redirige les écritures vers des appels de méthode. La validation vit à l'intérieur du setter.
class Product:
    def __init__(self, name, price):
        self._name  = name
        self._price = price

    @property
    def price(self):                 # getter
        return self._price

    @price.setter
    def price(self, value):          # setter — le nom doit correspondre au getter
        if not isinstance(value, int) or value < 0:
            raise ValueError("price must be a non-negative integer")
        self._price = value

    @property
    def label(self):                 # propriété calculée — valeur dérivée
        return f"{self._name} ({self._price})"


product = Product("T-shirt", 1500)
print(product.price)         # 1500              <- @property est invoqué
product.price = 2000         # <- @price.setter est invoqué
print(product.price)         # 2000
print(product.label)         # T-shirt (2000)    <- propriété calculée

Garde le nom du setter identique à celui du getter

Le price dans @price.setter doit correspondre au nom de méthode du @property def price précédent. Python interprète le décorateur comme « attache une version d'écriture au même objet price qui a déjà une version de lecture » — si le nom dérive, ils sont traités comme des choses séparées.

Réécris la même UserProfile de la Pratique 3 pour utiliser @property / @age.setter, et ajoute une propriété calculée age_group (under 18 / adult / senior).

① Définis class UserProfile: avec __init__(self, name, age) affectant _name / _age.

② Utilise @property def age(self): pour return self._age.

③ Utilise @age.setter def age(self, value): ; seulement quand isinstance(value, int) and 0 <= value <= 150 affecte self._age = value, sinon raise ValueError("age must be an integer in [0, 150]").

④ Utilise @property def age_group(self): pour retourner "under 18" si self._age < 18, "adult" si self._age < 65, sinon "senior" (pas de setter — lecture seule).

⑤ Crée user = UserProfile("Alice", 30), print user.age et user.age_group, puis user.age = 70 et print age_group une fois de plus.

Éditeur Python

Exécuter le code pour voir le résultat

Les trois piliers de la POO

Ce que l'encapsulation soutient
Encapsulation
  • Protection des données — sépare l'état interne de l'API publique via _x
  • Cohérence — concentre la validation dans les setters, en un seul endroit
  • Liberté d'implémentation — change les internes sans changer l'API publique
  • Style Pythonique — convention plus @property, pas une application du langage
Héritage
  • Réutiliser la machinerie d'un parent
Polymorphisme
  • Même nom de méthode, comportement différent par type
Encapsulation
  • Canaliser l'accès extérieur par un petit ensemble de portes
Héritage, polymorphisme et encapsulation sont les trois piliers de la POO. L'héritage réutilise, le polymorphisme unifie les sites d'appel, et l'encapsulation rend l'ensemble difficile à casser — assigner chaque rôle consciemment garde le design de classe propre.
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Quelle est l'affirmation la plus précise sur les variables privées de Python ?

Question 2Quel est le plus grand bénéfice d'utiliser @property et @xxx.setter ?

Question 3Dans class Account: tu as écrit self.__pin = 1234. Depuis l'extérieur, acc.__pin lève AttributeError. Quel nom est réellement stocké sur l'instance ?