Skip to main content
Niveau : Avancé

Module : Propriétés (@property)

1. Quoi : Les Propriétés

En Python, une propriété (property) permet de transformer une méthode de classe en un "attribut managé". Elle vous permet d'exécuter du code (logique de validation, calculs, etc.) chaque fois qu'un attribut est lu, modifié ou supprimé, tout en conservant une syntaxe d'accès simple et directe comme pour un attribut normal.

Une propriété est créée à l'aide du décorateur @property et, optionnellement, des décorateurs @<nom_propriete>.setter et @<nom_propriete>.deleter.

2. Pourquoi : Contrôler l'accès aux attributs

L'accès direct aux attributs (mon_objet.attribut = valeur) est simple, mais il n'offre aucun contrôle. Que se passe-t-il si vous voulez :

  • Valider une nouvelle valeur avant de l'assigner (ex: un email doit contenir un "@") ?
  • Calculer une valeur à la volée au lieu de la stocker (ex: l'âge d'une personne calculé à partir de sa date de naissance) ?
  • Déclencher une action lorsqu'un attribut est modifié ?

Les propriétés permettent de faire tout cela sans changer la manière dont l'utilisateur interagit avec votre objet. C'est une façon très "pythonic" de gérer l'encapsulation.

3. Comment : Getter, Setter, Deleter

A. Le "Getter" (@property)

Le décorateur @property est utilisé pour définir la méthode "getter". Cette méthode est appelée chaque fois que l'attribut est lu.

Exemple : Un attribut calculé

Imaginons une classe Rectangle avec une largeur et une hauteur. Nous voulons pouvoir accéder à son aire comme si c'était un attribut, mais elle doit être calculée à chaque fois.

class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height

@property
def area(self):
"""Calculates and returns the area of the rectangle."""
print("(Calculating area...)")
return self.width * self.height

rect = Rectangle(10, 5)

# On accède à 'area' comme un attribut, mais c'est la méthode 'area' qui est appelée.
print(f"The area is: {rect.area}") # (Calculating area...) The area is: 50

# Si on change les dimensions, l'aire est recalculée à la prochaine lecture.
rect.width = 12
print(f"The new area is: {rect.area}") # (Calculating area...) The new area is: 60

Notez qu'on appelle rect.area et non rect.area().

B. Le "Setter" (@<nom_propriete>.setter)

Le décorateur "setter" est utilisé pour contrôler ce qui se passe lorsqu'on essaie d'assigner une valeur à la propriété.

Exemple : Validation de données

Imaginons une classe Product avec un prix. Nous voulons nous assurer que le prix ne peut jamais être négatif.

Pour cela, on utilise un attribut "privé" (par convention, préfixé d'un underscore _) pour stocker la valeur réelle, et une propriété publique pour y accéder.

class Product:
def __init__(self, name, price):
self.name = name
# On appelle le setter dès l'initialisation
self.price = price

@property
def price(self):
"""Getter for the price."""
return self._price

@price.setter
def price(self, value):
"""Setter for the price, with validation."""
if value < 0:
raise ValueError("Price cannot be negative.")
# La valeur est stockée dans l'attribut "privé"
self._price = value

# Création d'un produit
try:
p = Product("Laptop", 1200)
print(f"{p.name} costs ${p.price}")

# Modification du prix (appelle le setter)
p.price = 1300
print(f"New price: ${p.price}")

# Tentative de modification invalide (appelle le setter, qui lève une erreur)
p.price = -50
except ValueError as e:
print(f"Error: {e}")

C. Le "Deleter" (@<nom_propriete>.deleter)

Le "deleter" est moins courant. Il est appelé lorsqu'on utilise l'instruction del sur la propriété.

class MyClass:
def __init__(self):
self._my_attribute = "some value"

@property
def my_attribute(self):
return self._my_attribute

@my_attribute.setter
def my_attribute(self, value):
self._my_attribute = value

@my_attribute.deleter
def my_attribute(self):
print("Deleting attribute...")
self._my_attribute = None

obj = MyClass()
print(obj.my_attribute) # "some value"

del obj.my_attribute # Appelle le deleter
print(obj.my_attribute) # None

4. Propriétés en lecture seule

Si vous définissez un "getter" (@property) mais pas de "setter", la propriété devient en lecture seule. Toute tentative d'assignation lèvera une AttributeError.

class Circle:
def __init__(self, radius):
self.radius = radius

@property
def diameter(self):
"""Diameter is a read-only calculated property."""
return self.radius * 2

c = Circle(10)
print(f"Radius: {c.radius}")
print(f"Diameter: {c.diameter}") # OK

# c.diameter = 30 # ❌ Lèvera une AttributeError: can't set attribute

C'est une excellente manière de protéger des attributs qui ne devraient pas être modifiés de l'extérieur.


Exercice 01 : Modélisation d'une Personne avec un Âge Contrôlé

Objectif

Cet exercice a pour but de vous faire utiliser le décorateur @property pour créer un attribut calculé en lecture seule (age) et un attribut contrôlé (email) avec un getter et un setter.

Contexte

Vous allez créer une classe Person qui stocke le nom et la date de naissance d'une personne.

  • L'âge ne sera pas stocké directement. Il sera calculé à la volée à partir de la date de naissance. Ce doit être une propriété en lecture seule.
  • L'email sera un attribut contrôlé. Le "setter" devra valider que la valeur fournie contient bien un caractère "@".

Énoncé

  1. Créez un nouveau fichier Python nommé person_model.py.

  2. Importez le module datetime en haut du fichier.

  3. Définissez la classe Person.

    • Son constructeur __init__ doit accepter name et birth_date. birth_date sera un objet datetime.date.
    • Stockez name et birth_date comme attributs.
    • Initialisez un attribut "privé" _email à None.
  4. Créez la propriété age en lecture seule.

    • Définissez une méthode age et décorez-la avec @property.
    • Cette méthode doit calculer l'âge de la personne en se basant sur la date d'aujourd'hui (datetime.date.today()) et self.birth_date.
    • Astuce : La différence entre deux dates est un objet timedelta. Vous pouvez obtenir le nombre de jours avec .days et diviser par 365.25 pour avoir une approximation de l'âge en années.
    • La méthode doit retourner l'âge sous forme d'entier.
    • Ne créez pas de setter pour age.
  5. Créez la propriété email contrôlée.

    • Getter : Définissez une méthode email décorée avec @property qui retourne la valeur de self._email.
    • Setter : Définissez une méthode email décorée avec @email.setter.
      • Cette méthode prend value en argument.
      • Elle doit vérifier si la chaîne value contient le caractère "@".
      • Si ce n'est pas le cas, elle doit lever une ValueError avec un message clair.
      • Si la validation réussit, elle assigne value à self._email.
  6. Testez votre classe :

    • Créez une instance de Person avec une date de naissance.
    • Affichez le nom et l'âge de la personne. L'âge doit être calculé correctement.
    • Essayez d'assigner une valeur à l'âge (person.age = 30). Vous devriez obtenir une AttributeError.
    • Assignez un email valide à la personne et affichez-le.
    • Essayez d'assigner un email invalide et assurez-vous qu'une ValueError est bien levée (vous pouvez utiliser un bloc try...except pour la démonstration).

Résultat Attendu

Name: Alice, Age: 30
---
Attempting to set age directly...
Error: can't set attribute 'age'
---
Setting a valid email...
Email: alice@example.com
---
Attempting to set an invalid email...
Error: Invalid email address provided.
Cliquez ici pour voir un exemple de code de solution
# person_model.py
import datetime

class Person:
def __init__(self, name, birth_date):
self.name = name
self.birth_date = birth_date
self._email = None

@property
def age(self):
"""Calculates the person's age in years. This is a read-only property."""
today = datetime.date.today()
# Calculate the difference in days and approximate the years
age_in_days = (today - self.birth_date).days
return int(age_in_days / 365.25)

@property
def email(self):
"""Getter for the email address."""
return self._email

@email.setter
def email(self, value):
"""Setter for the email address with validation."""
if "@" not in value:
raise ValueError("Invalid email address provided.")
self._email = value

# --- Testing ---

# Create a person instance (assuming today is around late 2025)
p = Person("Alice", datetime.date(1995, 5, 10))

# Test name and read-only age
print(f"Name: {p.name}, Age: {p.age}")
print("---")

# Test setting age (should fail)
print("Attempting to set age directly...")
try:
p.age = 30
except AttributeError as e:
print(f"Error: {e}")
print("---")

# Test setting a valid email
print("Setting a valid email...")
p.email = "alice@example.com"
print(f"Email: {p.email}")
print("---")

# Test setting an invalid email (should fail)
print("Attempting to set an invalid email...")
try:
p.email = "invalid-email.com"
except ValueError as e:
print(f"Error: {e}")