property en Python : un attribut calculé qui se lit comme les autres

Une property fait passer une méthode pour un attribut : lecture sans parenthèses, validation à l'écriture, sans rien changer pour l'appelant.
5 min de lecture
Believemy logo

Définition

Imaginez une classe Rectangle, avec une largeur et une hauteur enregistrées à la construction. Vous voulez que le code appelant puisse lire une aire, sans refaire la multiplication à chaque fois. Deux solutions existent, et aucune ne suffit vraiment. La première range l'aire dans un attribut classique, calculé à la construction : rapide à lire, mais périmé dès que la largeur change ensuite. La seconde expose une méthode aire(), toujours juste, mais qui oblige chaque appelant à écrire des parenthèses, révélant un détail qu'il n'a aucune raison de connaître.

property supprime ce choix impossible. Le décorateur @property, posé juste au-dessus d'une méthode, la fait passer pour un attribut : le résultat se lit sans parenthèses, comme une donnée rangée dans l'objet, alors qu'un calcul tourne derrière à chaque lecture.

PYTHON
class Rectangle:
    def __init__(self, largeur, hauteur):
        self.largeur = largeur
        self.hauteur = hauteur

    @property
    def aire(self):
        return self.largeur * self.hauteur

r = Rectangle(3, 4)
print(r.aire)   # 12, et non r.aire()

Pour le code appelant, r.aire ressemble en tout point à r.largeur : rien ne signale qu'une multiplication vient d'avoir lieu, parce que rien ne l'oblige à le savoir. L'attribut calculé reste aussi toujours cohérent avec les valeurs dont il dépend, ce qu'aucun attribut classique ne peut promettre.

Bon à savoir

Sous le capot, @property s'appuie sur le protocole des descripteurs, le mécanisme qui fait aussi fonctionner les méthodes ordinaires. C'est pourquoi une property doit vivre sur la classe : un descripteur ne se déclenche que depuis cet emplacement, jamais depuis un objet isolé.


Pourquoi ne pas écrire un attribut ordinaire

Beaucoup de langages poussent à entourer chaque donnée d'un accesseur et d'un mutateur, par précaution, dès le premier jour. Python fait l'inverse : l'attribut nu est exposé d'abord, et le jour où une validation devient nécessaire, une property glisse à sa place. La façade publique ne bouge pas : article.prix continue de se lire et de s'écrire comme avant, seul ce qui se passe derrière ces lignes a changé.

PYTHON
class Article:
    def __init__(self, prix):
        self.prix = prix          # passe déjà par le setter

    @property
    def prix(self):
        return self._prix

    @prix.setter
    def prix(self, valeur):
        if valeur < 0:
            raise ValueError("Un prix ne peut pas être négatif")
        self._prix = valeur

Le code écrit hier continue donc d'écrire article.prix = 12, sauf qu'un prix négatif fait désormais raise une ValueError au lieu de traverser silencieusement le reste de l'application. La valeur réelle, elle, est rangée dans _prix : le tiret bas signale par convention que cet attribut relève de la cuisine interne de la classe, et que rien à l'extérieur ne devrait s'y référer directement.


Lecture, écriture, suppression

Une property couvre trois accès distincts, chacun branché sur son propre décorateur, résumés ci-dessous. Seul le premier est obligatoire, les deux autres se rajoutent quand le besoin se présente.

DécorateurAccès couvertDéclenché par
@propertyLecturearticle.prix
@prix.setterÉcriturearticle.prix = 12
@prix.deleterSuppressiondel article.prix

Sans setter, la property reste en lecture seule : toute tentative d'affectation lève une AttributeError claire, plutôt que d'écraser silencieusement une valeur qui ne devait pas bouger. C'est une façon propre de rendre une donnée intouchable depuis l'extérieur, plus fiable qu'un commentaire demandant poliment de ne pas y toucher.


Les pièges classiques

Trois pièges reviennent régulièrement chez qui découvre property, et les connaître à l'avance épargne une heure de recherche perdue devant un message obscur.

Le premier est une récursion infinie, facile à écrire sans s'en rendre compte. Un setter qui écrit self.prix au lieu de self._prix se rappelle lui-même sans fin, puisque cette affectation redéclenche le setter au lieu de toucher l'attribut interne.

PYTHON
@prix.setter
def prix(self, valeur):
    self.prix = valeur   # se rappelle lui-même, RecursionError

Le programme s'arrête alors sur une RecursionError, dont le message ne mentionne jamais property : il faut remonter la pile d'appels pour comprendre que le setter s'appelle en boucle sur lui-même.

Le deuxième piège tient à l'endroit où elle est déclarée. Accrochée après coup à une seule instance plutôt qu'à la classe, elle est rangée comme un objet quelconque et ne se déclenche jamais : lire cet attribut renvoie la property elle-même, au lieu du résultat attendu.

Le troisième piège coûte le plus cher, parce qu'il ne se voit pas tout de suite. Une lecture qui ressemble à un attribut invite à la répéter sans y penser, y compris dans une boucle.

Attention

Si la property interroge une base de données ou relit un fichier, la même requête part à chaque lecture, cent fois pour cent tours de boucle. Elle doit rester bon marché, ou annoncer clairement son prix en redevenant une méthode ordinaire, avec ses parenthèses.


Questions fréquentes

Question

Faut-il mettre une property sur chaque attribut ?

Non, et c'est même l'erreur la plus répandue chez qui vient de découvrir l'outil. Une property qui se contente de renvoyer self._valeur sans rien vérifier ajoute du code sans rien apporter. Un attribut nu, ou une dataclass, suffit tant qu'aucun calcul ni aucune validation n'entrent en jeu.

Question

Comment éviter de recalculer une property coûteuse ?

Avec functools.cached_property, qui calcule au premier accès puis range le résultat dans l'objet, sans plus jamais y retoucher. Le revers mérite attention : la valeur ne se met plus à jour si les données dont elle dépend changent, ce qui la réserve aux objets qui ne bougent plus une fois construits.

Question

Quelle différence avec classmethod et staticmethod ?

Ces deux-là restent des méthodes appelées avec des parenthèses : classmethod reçoit la classe à la place de l'objet, staticmethod ne reçoit rien du tout. Une property ne change rien à ce que reçoit la méthode, seulement la façon d'y accéder.

Termes connexes

Découvrez notre glossaire Python

Parcourez les termes et définitions les plus couramment utilisés dans le domaine du développement avec Python.

Partager cet article

Tu veux nous aider ? Fais un lien vers cet article sur tes réseaux ou encore mieux : sur ton site, dans un article ou dans ta newsletter.