Maîtriser le décorateur @property de Python : getters et setters propres et compatibles
Maîtriser le décorateur @property de Python
Découvrez comment le décorateur intégré @property de Python vous permet d'écrire des getters et setters concis et maintenables tout en gardant votre API rétrocompatible.
Le décorateur @property de Python est un outil puissant pour encapsuler l'accès aux attributs. Il vous permet de définir une logique personnalisée qui s'exécute chaque fois qu'un attribut est lu ou écrit, sans forcer les appelants à changer leur code. Dans les sections suivantes, nous parcourrons un exemple réel — la gestion des conversions de température — montrerons comment imposer des contraintes, et expliquerons pourquoi @property est la solution idiomatique.
Une classe sans getters et setters
Supposons que nous ayons besoin d'une classe simple qui stocke la température en Celsius et peut convertir en Fahrenheit :
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
Son utilisation est directe :
# Créer un nouvel objet
humain = Celsius()
# Définir la température
humain.temperature = 37
# Lire la température
print(humain.temperature)
# Convertir en Fahrenheit
print(humain.to_fahrenheit())
Sortie
37 98.60000000000001
Le résultat en virgule flottante est attendu en raison de la représentation binaire des décimaux. Quand vous inspectez humain.__dict__, vous verrez que Python stocke la valeur dans une entrée de dictionnaire simple :
>>> humain.__dict__
{'temperature': 37}
Ajouter des contraintes avec getters et setters
Les objets du monde réel ont souvent besoin de validation. Par exemple, les températures ne peuvent pas descendre en dessous du zéro absolu (-273.15 °C). Une façon directe est de cacher l'attribut et d'exposer des méthodes getter/setter explicites :
class Celsius:
def __init__(self, temperature=0):
self.set_temperature(temperature)
def to_fahrenheit(self):
return (self.get_temperature() * 1.8) + 32
# getter
def get_temperature(self):
return self._temperature
# setter
def set_temperature(self, value):
if value < -273.15:
raise ValueError("Température en dessous de -273.15 impossible.")
self._temperature = value
Maintenant, la classe impose la borne inférieure, mais tout code existant qui accédait à obj.temperature doit être mis à jour pour appeler obj.get_temperature() et obj.set_temperature(val). C'est une refactorisation majeure pour de grandes bases de code.
De plus, les variables « privées » de Python ne sont qu'une convention de nommage — _temperature peut encore être accédé directement, brisant l'encapsulation. C'est là que @property brille.
Utiliser la classe property
Python offre une classe property qui lie les fonctions getter et setter à un nom d'attribut :
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
# getter
def get_temperature(self):
print("Récupération de la valeur...")
return self._temperature
# setter
def set_temperature(self, value):
print("Définition de la valeur...")
if value < -273.15:
raise ValueError("Température en dessous de -273.15 impossible")
self._temperature = value
# exposer comme une propriété
temperature = property(get_temperature, set_temperature)
Avec cette configuration, le code suivant se comporte exactement comme la classe simple originale, mais avec validation en coulisses :
humain = Celsius(37)
print(humain.temperature) # déclenche le getter
print(humain.to_fahrenheit()) # utilise le getter en interne
humain.temperature = -300 # déclenche le setter et lève ValueError
Sortie
Définition de la valeur... Récupération de la valeur... 37 Récupération de la valeur... 98.60000000000001 Définition de la valeur... Traceback (most recent call last): ... ValueError: Température en dessous de -273 impossible
Remarquez comment le code qui accède à temperature reste inchangé ; seule l'implémentation sous-jacente a gagné en validation.
Astuce : Les données réelles vivent dans _temperature ; temperature n'est qu'une interface pratique et sûre.
Le décorateur @property
Bien que la classe property fonctionne, la syntaxe de décorateur de Python offre une façon plus propre et plus lisible d'atteindre le même résultat. Le décorateur crée automatiquement l'objet property et attache le getter et setter au nom de l'attribut.
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
@property
def temperature(self):
print("Récupération de la valeur...")
return self._temperature
@temperature.setter
def temperature(self, value):
print("Définition de la valeur...")
if value < -273.15:
raise ValueError("Température en dessous de -273 impossible")
self._temperature = value
L'utilisation reste la même :
humain = Celsius(37)
print(humain.temperature)
print(humain.to_fahrenheit())
# Tentative de définir une température invalide
try:
Celsius(-300)
except ValueError as e:
print(e)
Sortie
Définition de la valeur... Récupération de la valeur... 37 Récupération de la valeur... 98.60000000000001 Définition de la valeur... Traceback (most recent call last): ... ValueError: Température en dessous de -273 impossible
L'utilisation de @property garde l'espace de noms propre, élimine le besoin de noms de méthodes getter/setter séparés, et est le motif recommandé pour la plupart des développeurs Python.
Python
- Maîtriser les types de données Python : Guide pratique
- Maîtriser les opérateurs Python : Guide complet
- Maîtriser l'instruction pass en Python : Guide pratique
- Maîtriser les arguments de fonctions Python : Positionnels, nommés et paramètres par défaut
- Maîtriser les dictionnaires Python : création, manipulation et techniques avancées
- Maîtriser l'héritage Python : concepts, syntaxe et exemples pratiques
- Maîtriser la surcharge d'opérateurs en Python : guide pratique
- Maîtriser les itérateurs Python : fonctionnement et création des vôtres
- Les fermetures Python expliquées : comment les fonctions imbriquées capturent les variables