W3docs

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 impossible

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

Le 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 automatically

Comme 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 setter

C'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 = value

Maintenant, 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 zero

Rè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 + 32

fahrenheit 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.0

Comme 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 positive

Ajout 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 = -1

Plus 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 negative

Tout 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 years

La 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)     # None

C'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 again

Stockez 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 attribute

Setter 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 negative

Les 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

SyntaxeCe qu'elle fait
@propertyDéfinit le getter ; l'attribut devient en lecture seule jusqu'à l'ajout d'un setter
@<name>.setterDéfinit le setter ; l'attribut devient lisible et modifiable
@<name>.deleterDé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.fgetLa fonction getter sous-jacente
ClassName.prop.fsetLa fonction setter sous-jacente (None si pas de setter)
ClassName.prop.fdelLa fonction deleter sous-jacente (None si pas de deleter)

Pratique

Pratique
Which decorator do you use to define a setter for a property named `age`?
Which decorator do you use to define a setter for a property named `age`?
Pratique
What happens when you assign to a property that has only a getter defined?
What happens when you assign to a property that has only a getter defined?
Pratique
You have a plain public attribute `self.radius` in v1 of a class. In v2 you add a `@property` for `radius`. What happens to existing callers that write `obj.radius = 5`?
You have a plain public attribute `self.radius` in v1 of a class. In v2 you add a `@property` for `radius`. What happens to existing callers that write `obj.radius = 5`?
Was this page helpful?