Définir des propriétés avec @property¶
Introduction¶
Le décorateur @property est un outil puissant de Python qui permet de contrôler l’accès aux attributs d’une classe
comme s’ils étaient des attributs publics, tout en permettant une validation ou un traitement personnalisé. Il est
particulièrement utile pour :
- Encapsulation : Protéger les données internes
- Validation : Vérifier les valeurs avant affectation
- Calcul dynamique : Calculer des valeurs à la demande
- Interface propre : Offrir une API simple tout en gardant le contrôle
Structure de base d’une propriété¶
Une propriété est composée de trois parties :
class MaClasse:
def __init__(self):
self._mon_attribut = None # Attribut privé (convention)
@property
def mon_attribut(self): # Getter
"""Docstring pour le getter"""
return self._mon_attribut
@mon_attribut.setter
def mon_attribut(self, valeur): # Setter
"""Docstring pour le setter"""
if not self._valider_valeur(valeur):
raise ValueError("Valeur invalide")
self._mon_attribut = valeur
def _valider_valeur(self, valeur):
"""Méthode de validation interne"""
return True # À surcharger
Exemple 1 : Classe Personne avec validation¶
Voici la classe Personne modifiée pour utiliser des propriétés :
class Personne:
def __init__(self, nom, age):
self._nom = None
self._age = None
self.nom = nom # Utilise le setter pour valider
self.age = age
@property
def nom(self):
"""Retourne le nom de la personne."""
return self._nom
@nom.setter
def nom(self, valeur):
"""Définie le nom avec validation."""
if not isinstance(valeur, str):
raise TypeError("Le nom doit être une chaîne de caractères")
valeur = valeur.strip()
if len(valeur) == 0:
raise ValueError("Le nom ne peut pas être vide après suppression des espaces")
self._nom = valeur
@property
def age(self):
"""Retourne l'âge de la personne."""
return self._age
@age.setter
def age(self, valeur):
"""Définie l'âge avec validation."""
if not isinstance(valeur, int) or valeur < 0:
raise ValueError("L'âge doit être un entier positif")
self._age = valeur
def saluer(self):
return f"Bonjour, je m'appelle {self.nom} et j'ai {self.age} ans."
def __repr__(self):
return f"Personne(nom='{self.nom}', age={self.age})"
def __str__(self):
return self.saluer()
def __eq__(self, autre):
if isinstance(autre, Personne):
return self.nom == autre.nom and self.age == autre.age
return False
def __hash__(self):
return hash((self.nom, self.age))
def __lt__(self, autre):
if isinstance(autre, Personne):
return (self.nom, self.age) < (autre.nom, autre.age)
return False
# Exemples d'utilisation
try:
p1 = Personne("Alice", 30)
print(p1.saluer()) # Bonjour, je m'appelle Alice et j'ai 30 ans.
p2 = Personne("", -5) # Va lever une exception pour le nom vide
except (ValueError, TypeError) as e:
print(f"Erreur lors de la création: {e}")
try:
p1.nom = "Bob" # Modification valide
p1.age = -10 # Va lever une exception
except ValueError as e:
print(f"Erreur lors de la modification: {e}") # L'âge doit être un entier positif
p1.age = 35 # Modification valide
print(p1) # Bonjour, je m'appelle Bob et j'ai 35 ans.
Points clés pour cet exemple :¶
-
Validation du nom :
- Vérifie que c’est une chaîne de caractères
- Supprime les espaces avec
strip() - Vérifie que la longueur est > 0 après nettoyage
-
Validation de l’âge :
- Doit être un entier (
int) - Doit être positif (>= 0)
- Doit être un entier (
-
Comportement lors des erreurs :
- Les exceptions sont levées immédiatement
- L’objet reste dans un état cohérent (pas d’attributs partiellement définis)
Exemple 2 : Classe Point avec validation¶
Voici l’exemple complet de la classe Point avec propriétés :
class Point:
def __init__(self, x=0, y=0):
self._x = None
self._y = None
self.x = x # Utilise le setter pour valider
self.y = y
@property
def x(self):
"""Retourne la coordonnée x du point."""
return self._x
@x.setter
def x(self, valeur):
"""Définie la coordonnée x avec validation."""
if not isinstance(valeur, (int, float)):
raise TypeError("La coordonnée x doit être un nombre")
if valeur < 0:
raise ValueError("La coordonnée x doit être positive ou nulle")
self._x = float(valeur) # On stocke toujours en float pour la cohérence
@property
def y(self):
"""Retourne la coordonnée y du point."""
return self._y
@y.setter
def y(self, valeur):
"""Définie la coordonnée y avec validation."""
if not isinstance(valeur, (int, float)):
raise TypeError("La coordonnée y doit être un nombre")
if valeur < 0:
raise ValueError("La coordonnée y doit être positive ou nulle")
self._y = float(valeur)
def distance(self, autre_point):
"""Calcule la distance euclidienne entre deux points."""
if not isinstance(autre_point, Point):
raise TypeError("L'argument doit être un objet Point")
return ((self.x - autre_point.x) ** 2 + (self.y - autre_point.y) ** 2) ** 0.5
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
def __str__(self):
return f"({self.x}, {self.y})"
def __eq__(self, autre):
if isinstance(autre, Point):
return self.x == autre.x and self.y == autre.y
return False
# Exemples d'utilisation
try:
p1 = Point(3, 4)
print(p1) # (3.0, 4.0)
p2 = Point(-1, 5) # Va lever une exception pour x négatif
except ValueError as e:
print(f"Erreur: {e}") # La coordonnée x doit être positive ou nulle
p3 = Point(0, 0)
print(p3.distance(p1)) # 5.0 (distance entre (0,0) et (3,4))
# Modification des valeurs
try:
p1.x = "cinq" # Va lever une exception pour type incorrect
except TypeError as e:
print(f"Erreur: {e}") # La coordonnée x doit être un nombre
p1.y = 7.5 # Modification valide
print(p1) # (3.0, 7.5)
Points clés pour cet exemple :¶
-
Validation des types :
- Accepte à la fois
intetfloat - Convertit toujours en
floatpour la cohérence interne
- Accepte à la fois
-
Validation des valeurs :
- Les coordonnées doivent être >= 0
- Messages d’erreur descriptifs
-
Méthodes supplémentaires :
- La méthode
distance()montre comment utiliser les propriétés dans d’autres méthodes - Implémentation de
__repr__et__str__pour une meilleure représentation
- La méthode
Cas particuliers et bonnes pratiques¶
1. Propriétés en lecture seule¶
Pour créer une propriété accessible uniquement en lecture :
class Rectangle:
def __init__(self, largeur, hauteur):
self._largeur = largeur
self._hauteur = hauteur
@property
def aire(self):
"""Calcule l'aire du rectangle."""
return self._largeur * self._hauteur
# Utilisation
r = Rectangle(5, 10)
print(r.aire) # 50
# r.aire = 20 # Va lever une AttributeError
2. Propriétés calculées¶
Les propriétés peuvent calculer des valeurs dynamiquement :
class Cercle:
def __init__(self, rayon):
self._rayon = rayon
@property
def rayon(self):
return self._rayon
@rayon.setter
def rayon(self, valeur):
if valeur < 0:
raise ValueError("Le rayon doit être positif")
self._rayon = valeur
@property
def diametre(self):
"""Retourne le diamètre (calculé à partir du rayon)."""
return 2 * self._rayon
@property
def circonference(self):
"""Retourne la circonférence."""
return 2 * 3.14159 * self._rayon
# Utilisation
c = Cercle(5)
print(c.diametre) # 10 (calculé dynamiquement)
3. Gestion des exceptions personnalisées¶
Pour une meilleure gestion des erreurs :
class InvalidAgeError(ValueError):
"""Exception levée lorsque l'âge est invalide."""
pass
class InvalidNameError(ValueError):
"""Exception levée lorsque le nom est invalide."""
pass
class Personne:
# ... (même structure que précédemment) ...
@nom.setter
def nom(self, valeur):
if not isinstance(valeur, str):
raise TypeError("Le nom doit être une chaîne de caractères")
valeur = valeur.strip()
if len(valeur) == 0:
raise InvalidNameError("Le nom ne peut pas être vide après suppression des espaces")
self._nom = valeur
@age.setter
def age(self, valeur):
if not isinstance(valeur, int) or valeur < 0:
raise InvalidAgeError(f"L'âge doit être un entier positif (reçu: {valeur})")
self._age = valeur
# Utilisation avec gestion d'exceptions spécifiques
try:
p = Personne("", -5)
except InvalidNameError as e:
print(f"Erreur de nom: {e}")
except InvalidAgeError as e:
print(f"Erreur d'âge: {e}")
4. Propriétés avec valeurs par défaut¶
class Configuration:
def __init__(self):
self._debug = False
self._timeout = 30
@property
def debug(self):
"""Mode débogage."""
return self._debug
@debug.setter
def debug(self, valeur):
if not isinstance(valeur, bool):
raise TypeError("Le mode debug doit être un booléen")
self._debug = valeur
@property
def timeout(self):
"""Timeout en secondes."""
return self._timeout
@timeout.setter
def timeout(self, valeur):
if not isinstance(valeur, (int, float)) or valeur <= 0:
raise ValueError("Le timeout doit être un nombre positif")
self._timeout = float(valeur)
# Utilisation avec valeurs par défaut
config = Configuration()
print(config.debug) # False (valeur par défaut)
print(config.timeout) # 30.0 (valeur par défaut)
Avantages des propriétés¶
- Encapsulation : Protège les données internes contre les modifications incorrectes
- Flexibilité : Permet de changer l’implémentation interne sans affecter le code client
- Validation : Garantit que les objets restent dans un état valide
- Interface propre : Offre une API simple et intuitive
- Documentation : Les docstrings des propriétés apparaissent dans
help()et les outils IDE
Pièges à éviter¶
-
Modification directe de l’attribut privé :
-
Oublier le setter : Si vous définissez un getter mais pas de setter, l’attribut devient en lecture seule.
-
Performance : Les propriétés calculées sont recalculées à chaque accès - évitez pour des calculs coûteux.
-
Cohérence entre getter et setter : Assurez-vous que le setter met toujours l’objet dans un état valide.
Exercice pratique¶
-
Créez une classe
CompteBancaireavec :- Un solde (doit être >= 0)
- Un titulaire (ne peut pas être vide après strip())
- Une méthode
deposer()qui ajoute au solde - Une méthode
retirer()qui soustrait du solde (ne peut pas dépasser le solde)
-
Créez une classe
Etudiantavec :- Un nom (comme dans Personne)
- Une liste de notes (doit être une liste d’entiers entre 0 et 20)
- Une propriété calculée
moyennequi retourne la moyenne des notes
-
Modifiez la classe
Pointpour ajouter :- Une méthode
deplacer()qui prend un autre Point comme argument - Une validation qui empêche le déplacement vers des coordonnées négatives
- Une méthode
Conclusion¶
Les propriétés sont un outil essentiel en Python pour créer des classes robustes et bien encapsulées. Elles permettent de :
- Valider les données entrantes
- Calculer des valeurs dynamiquement
- Maintenir une interface propre tout en gardant le contrôle
- Documenter clairement l’API de la classe
En maîtrisant les propriétés, vous serez capable de créer des classes Python plus professionnelles et sûres.