Python @property : Getters et Setters
Apprenez le décorateur @property de Python : créez des getters, setters, deleters et attributs calculés avec une syntaxe claire et un contrôle de validation complet.
Le décorateur @property est le mécanisme intégré de Python pour transformer une méthode en attribut géré. Au lieu d'écrire des méthodes get_x() et set_x() comme dans d'autres langages, vous écrivez un accès à l'attribut d'apparence normale (obj.x) tout en gardant un contrôle total sur ce qui se passe lorsque cet attribut est lu, écrit ou supprimé.
Ce chapitre couvre :
- Pourquoi les propriétés existent et quand les utiliser
- Créer une propriété en lecture seule avec
@property - Ajouter un setter avec
@<name>.setter - Ajouter un deleter avec
@<name>.deleter - Les propriétés calculées (dérivées)
- Mettre à niveau un attribut simple en propriété sans casser les appelants
- La fonction intégrée
property()— le mécanisme sous-jacent du décorateur - Comment les propriétés fonctionnent en tant que descripteurs (bref aperçu du fonctionnement interne)
- Les pièges courants
Avant de lire, assurez-vous d'être à l'aise avec les classes et objets Python. Les propriétés sont un outil clé pour l'encapsulation Python. Pour les méthodes de classe et statiques, voir @staticmethod et @classmethod.
Pourquoi les propriétés existent
Considérez une classe qui stocke une température en Celsius. Une implémentation naïve expose directement la valeur interne :
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
t = Temperature(25)
t.celsius = -5000 # nothing stops this — physically impossibleLe problème : rien n'empêche les appelants de définir une température inférieure au zéro absolu (−273,15 °C). Vous pourriez ajouter une méthode set_celsius() avec validation, mais les appelants doivent alors modifier leur code de t.celsius = 100 vers t.set_celsius(100) — un changement d'API incompatible.
@property résout cela proprement. Vous conservez la syntaxe t.celsius = 100 tout en ajoutant une couche de contrôle en coulisses.
Getter de base : accès en lecture seule
L'utilisation la plus simple de @property est un attribut en lecture seule soutenu par une variable privée :
class Temperature:
def __init__(self, celsius):
self._celsius = celsius # store in a private attribute
@property
def celsius(self):
return self._celsiusLe décorateur @property fait apparaître celsius comme un attribut ordinaire à l'appelant :
t = Temperature(25)
print(t.celsius) # 25 — no parentheses; Python calls the getter automaticallyComme il n'y a pas de setter, tenter d'y affecter une valeur lève une erreur :
t.celsius = 30
# AttributeError: property 'celsius' of 'Temperature' object has no setterC'est la bonne façon de modéliser une valeur qui ne doit être définie qu'à la construction ou via des méthodes spécifiques.
Ajout d'un setter avec validation
Décorez une deuxième méthode avec @<property_name>.setter pour gérer les écritures :
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 = valueMaintenant, la lecture et l'écriture fonctionnent avec la syntaxe d'attribut ordinaire :
t = Temperature(25)
print(t.celsius) # 25
t.celsius = 100
print(t.celsius) # 100
t.celsius = -300 # ValueError: Temperature below absolute zeroRègle clé : le setter et le getter doivent partager le même nom (celsius dans les deux cas). Le décorateur @celsius.setter lie la nouvelle méthode à l'objet propriété celsius existant.
Propriétés calculées
Une propriété ne doit pas nécessairement correspondre à un attribut stocké. Elle peut calculer une valeur à la volée à partir d'autres données :
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 + 32fahrenheit n'a pas de variable de stockage — elle dérive sa valeur de _celsius à chaque lecture :
t = Temperature(0)
print(t.fahrenheit) # 32.0
t.celsius = 100
print(t.fahrenheit) # 212.0Comme il n'y a pas de @fahrenheit.setter, tenter d'écrire t.fahrenheit = 100 lève une AttributeError. Les propriétés calculées sont naturellement en lecture seule, sauf si vous ajoutez explicitement un setter.
Un exemple concret de propriété calculée
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError('Width must be positive')
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError('Height must be positive')
self._height = value
@property
def area(self):
return self._width * self._height # computed; no setter
@property
def perimeter(self):
return 2 * (self._width + self._height) # computed; no setter
r = Rectangle(4, 5)
print(r.area) # 20
print(r.perimeter) # 18
r.width = 10
print(r.area) # 50
r.width = -1 # ValueError: Width must be positiveAjout d'un deleter
Le décorateur @<property_name>.deleter vous permet d'exécuter du code lorsque l'appelant utilise del obj.attr :
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
@celsius.deleter
def celsius(self):
print('Deleting celsius')
del self._celsius
t = Temperature(25)
del t.celsius # Deleting celsius
print(t.celsius) # AttributeError: 'Temperature' object has no attribute '_celsius'Les deleters sont moins couramment utilisés que les getters et les setters. Ils sont utiles lorsque :
- Vous supprimez une valeur mise en cache pour forcer son recalcul lors du prochain accès.
- Vous libérez explicitement des ressources liées à un attribut.
- Vous appliquez la règle que, une fois supprimée, une valeur ne peut pas être relue sans être réaffectée.
Mettre à niveau un attribut simple en propriété
L'un des plus grands avantages pratiques de @property est que vous pouvez commencer avec un attribut public simple et ajouter une validation plus tard sans modifier le code appelant. C'est parfois appelé le principe d'accès uniforme.
# Version 1 — plain attribute, no validation
class Circle:
def __init__(self, radius):
self.radius = radius
c = Circle(5)
print(c.radius) # 5
c.radius = 10 # works, but nothing stops c.radius = -1Plus tard, vous avez besoin de validation. Avec @property, vous pouvez l'ajouter sans toucher aux appelants :
# Version 2 — property with validation; public interface unchanged
import math
class Circle:
def __init__(self, radius):
self.radius = radius # this now calls the setter
@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
@property
def area(self):
return math.pi * self._radius ** 2
c = Circle(5)
print(c.radius) # 5
print(f'{c.area:.4f}') # 78.5398
c.radius = 10
print(c.radius) # 10
c.radius = -1 # ValueError: Radius cannot be negativeTout code existant qui lit ou écrit c.radius continue de fonctionner sans modification.
La fonction intégrée property()
@property est du sucre syntaxique pour la fonction intégrée property(). Ces deux définitions sont équivalentes :
# --- decorator style (recommended) ---
class Person:
def __init__(self, age):
self._age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, int) or value < 0:
raise ValueError('Age must be a non-negative integer')
self._age = value# --- property() style (explicit) ---
class Person:
def __init__(self, age):
self._age = age
def _get_age(self):
return self._age
def _set_age(self, value):
if not isinstance(value, int) or value < 0:
raise ValueError('Age must be a non-negative integer')
self._age = value
def _del_age(self):
del self._age
age = property(_get_age, _set_age, _del_age, 'The person\'s age in years')property(fget, fset, fdel, doc) prend jusqu'à quatre arguments : une fonction getter, une fonction setter, une fonction deleter et une docstring. Chacun d'eux peut être None.
p = Person(30)
print(p.age) # 30
p.age = 31
print(p.age) # 31
print(Person.age.__doc__) # The person's age in yearsLa forme avec décorateur est plus claire et constitue la recommandation standard. L'appel explicite à property() est utile lorsque vous souhaitez passer la docstring sans un bloc de décorateur sur plusieurs lignes, ou lorsque les fonctions d'accès existent déjà sous un autre nom.
Fonctionnement des propriétés : bref aperçu des descripteurs
En interne, property est un descripteur — un objet qui définit __get__, __set__ et __delete__ sur la classe. Lorsque Python cherche obj.attr, il vérifie si l'attribut sur la classe est un descripteur et, si c'est le cas, appelle son __get__ au lieu de renvoyer la valeur directement.
Vous pouvez le voir en inspectant l'objet propriété sur la classe :
class Square:
def __init__(self, side):
self._side = side
@property
def side(self):
return self._side
@side.setter
def side(self, value):
if value < 0:
raise ValueError('Side must be non-negative')
self._side = value
print(type(Square.side)) # <class 'property'>
print(Square.side.fget) # <function Square.side at 0x...>
print(Square.side.fset) # <function Square.side at 0x...>
print(Square.side.fdel) # NoneC'est pourquoi lire Square.side renvoie l'objet propriété lui-même (descripteur accédé sur la classe), tandis que lire s.side sur une instance déclenche __get__ et renvoie l'entier. Le protocole de descripteur est le même mécanisme utilisé par classmethod, staticmethod et les fonctions elles-mêmes. Pour une plongée plus profonde, voir les méthodes magiques Python.
Pièges courants
Récursion infinie : oublier le tiret bas
Une erreur très courante est d'utiliser le même nom pour la propriété et l'attribut de stockage :
class Bad:
@property
def value(self):
return self.value # RecursionError! This calls the getter again
@value.setter
def value(self, v):
self.value = v # RecursionError! This calls the setter againStockez toujours la valeur de sauvegarde dans un nom différent, par convention préfixé par un tiret bas :
class Good:
@property
def value(self):
return self._value # reads the private attribute
@value.setter
def value(self, v):
self._value = v # writes the private attributeSetter défini avant le getter
Le décorateur setter @celsius.setter référence l'objet propriété celsius, qui doit exister en premier. Définissez toujours le getter (@property) avant le setter et le deleter dans le corps de la classe.
__init__ appelle automatiquement le setter
Lorsque vous écrivez self.radius = radius dans __init__, Python appelle le setter (s'il en existe un). C'est généralement ce que vous voulez — la validation s'exécute aussi lors de la construction. Mais cela signifie que votre setter doit gérer l'affectation initiale correctement :
class Circle:
def __init__(self, radius):
self.radius = radius # triggers the setter — validation applies here too
@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
Circle(-1) # ValueError: Radius cannot be negativeLes propriétés sont au niveau de la classe, pas de l'instance
Vous ne pouvez pas ajouter une propriété à une seule instance comme vous le feriez avec des attributs normaux. Les propriétés sont définies sur la classe et s'appliquent à toutes les instances. Si vous avez besoin d'une personnalisation d'attribut par instance, voir les dataclasses Python ou utilisez une approche basée sur __slots__.
Référence rapide
| Syntaxe | Ce qu'elle fait |
|---|---|
@property | Définit le getter ; l'attribut devient en lecture seule jusqu'à l'ajout d'un setter |
@<name>.setter | Définit le setter ; l'attribut devient lisible et modifiable |
@<name>.deleter | Définit le deleter ; del obj.attr déclenche cette méthode |
property(fget, fset, fdel, doc) | Équivalent intégré sans syntaxe de décorateur |
ClassName.prop.fget | La fonction getter sous-jacente |
ClassName.prop.fset | La fonction setter sous-jacente (None si pas de setter) |
ClassName.prop.fdel | La fonction deleter sous-jacente (None si pas de deleter) |