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é
-
Créez un nouveau fichier Python nommé
person_model.py. -
Importez le module
datetimeen haut du fichier. -
Définissez la classe
Person.- Son constructeur
__init__doit accepternameetbirth_date.birth_datesera un objetdatetime.date. - Stockez
nameetbirth_datecomme attributs. - Initialisez un attribut "privé"
_emailàNone.
- Son constructeur
-
Créez la propriété
ageen lecture seule.- Définissez une méthode
ageet 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()) etself.birth_date. - Astuce : La différence entre deux dates est un objet
timedelta. Vous pouvez obtenir le nombre de jours avec.dayset 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.
- Définissez une méthode
-
Créez la propriété
emailcontrôlée.- Getter : Définissez une méthode
emaildécorée avec@propertyqui retourne la valeur deself._email. - Setter : Définissez une méthode
emaildécorée avec@email.setter.- Cette méthode prend
valueen argument. - Elle doit vérifier si la chaîne
valuecontient le caractère"@". - Si ce n'est pas le cas, elle doit lever une
ValueErroravec un message clair. - Si la validation réussit, elle assigne
valueàself._email.
- Cette méthode prend
- Getter : Définissez une méthode
-
Testez votre classe :
- Créez une instance de
Personavec 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 uneAttributeError. - Assignez un email valide à la personne et affichez-le.
- Essayez d'assigner un email invalide et assurez-vous qu'une
ValueErrorest bien levée (vous pouvez utiliser un bloctry...exceptpour la démonstration).
- Créez une instance de
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}")