Le décorateur @property en Python: ses cas d'utilisation, ses avantages et sa syntaxe

🔹 Meet Properties

Bienvenue! Dans cet article, vous apprendrez à travailler avec le @propertydécorateur en Python.

Tu vas apprendre:

  • Les avantages de travailler avec des propriétés en Python.
  • Les bases des fonctions de décorateur: ce qu'elles sont et comment elles sont liées à @property.
  • Comment vous pouvez utiliser @property pour définir des getters, des setters et des suppresseurs.

1️⃣ Avantages des propriétés en Python

Commençons par un peu de contexte. Pourquoi utiliseriez-vous des propriétés en Python?

Les propriétés peuvent être considérées comme la manière «pythonique» de travailler avec les attributs car:

  • La syntaxe utilisée pour définir les propriétés est très concise et lisible.
  • Vous pouvez accéder aux attributs d'instance exactement comme s'il s'agissait d'attributs publics tout en utilisant la «magie» des intermédiaires (getters et setters) pour valider de nouvelles valeurs et éviter d'accéder ou de modifier directement les données.
  • En utilisant @property, vous pouvez "réutiliser" le nom d'une propriété pour éviter de créer de nouveaux noms pour les getters, les setters et les suppresseurs.

Ces avantages font des propriétés un outil vraiment génial pour vous aider à écrire un code plus concis et plus lisible. ?

2️⃣ Introduction aux décorateurs

Une fonction décoratrice est essentiellement une fonction qui ajoute de nouvelles fonctionnalités à une fonction qui est passée en argument. Utiliser une fonction décoratrice, c'est comme ajouter des pépites de chocolat à une glace?. Cela nous permet d'ajouter de nouvelles fonctionnalités à une fonction existante sans la modifier.

Dans l'exemple ci-dessous, vous pouvez voir à quoi ressemble une fonction de décorateur typique en Python:

def decorator(f): def new_function(): print("Extra Functionality") f() return new_function @decorator def initial_function(): print("Initial Functionality") initial_function()

Analysons ces éléments en détail:

  • On trouve d'abord la fonction décoratrice def decorator(f)(les sprinkles ✨) qui prend une fonction fcomme argument.
def decorator(f): def new_function(): print("Extra Functionality") f() return new_function
  • Cette fonction décorateur a une fonction imbriquée, new_function. Notez comment fest appelé à l'intérieur new_functionpour obtenir la même fonctionnalité tout en ajoutant de nouvelles fonctionnalités avant l'appel de fonction (nous pourrions également ajouter de nouvelles fonctionnalités après l'appel de fonction).
  • La fonction décoratrice elle-même renvoie la fonction imbriquée new_function.
  • Puis (ci-dessous), on retrouve la fonction qui sera décorée (la glace?) initial_function. Notez la syntaxe très particulière ( @decorator) au-dessus de l'en-tête de la fonction.
@decorator def initial_function(): print("Initial Functionality") initial_function()

Si nous exécutons le code, nous voyons cette sortie:

Extra Functionality Initial Functionality

Remarquez comment la fonction décoratrice s'exécute même si nous n'appelons que initial_function(). C'est la magie d'ajouter @decorator?.

💡Remarque: En général, nous écririons @en remplaçant le nom de la fonction décoratrice après le symbole @.

Je sais que vous vous demandez peut-être: comment cela est-il lié à la propriété @? @Property est un décorateur intégré pour la fonction property () en Python. Il est utilisé pour donner des fonctionnalités "spéciales" à certaines méthodes pour les faire agir comme des getters, des setters ou des suppresseurs lorsque nous définissons des propriétés dans une classe.

Maintenant que vous êtes familier avec les décorateurs, voyons un vrai scénario d'utilisation de @property!

🔸 Scénario du monde réel: @property

Disons que cette classe fait partie de votre programme. Vous modélisez une maison avec une Houseclasse (pour le moment, la classe n'a qu'un attribut d'instance de prix défini):

class House: def __init__(self, price): self.price = price

Cet attribut d'instance est public car son nom n'a pas de trait de soulignement en tête. Étant donné que l'attribut est actuellement public, il est très probable que vous et d'autres développeurs de votre équipe avez accédé et modifié l'attribut directement dans d'autres parties du programme en utilisant la notation par points, comme ceci:

# Access value obj.price # Modify value obj.price = 40000

💡 Astuce: obj représente une variable qui référence une instance de House.

Jusqu'à présent, tout fonctionne très bien, non? Mais disons qu'on vous demande de rendre cet attribut protégé (non public) et de valider la nouvelle valeur avant de l'attribuer . Plus précisément, vous devez vérifier si la valeur est un flottant positif. Comment feriez-vous cela? Voyons voir.

Changer votre code

À ce stade, si vous décidez d'ajouter des getters et des setters, vous et votre équipe allez probablement paniquer?. En effet, chaque ligne de code qui accède ou modifie la valeur de l'attribut devra être modifiée pour appeler respectivement le getter ou le setter. Sinon, le code se cassera ⚠️.

# Changed from obj.price obj.get_price() # Changed from obj.price = 40000 obj.set_price(40000)

Mais ... Les propriétés viennent à la rescousse! Avec @property, vous et votre équipe n'aurez pas besoin de modifier l'une de ces lignes car vous pourrez ajouter des getters et des setters "en coulisses" sans affecter la syntaxe que vous utilisiez pour accéder ou modifier l'attribut lorsqu'il était public.

Génial, non?  

🔹 @property: syntaxe et logique

Si vous décidez d'utiliser @property, votre classe ressemblera à l'exemple ci-dessous:

class House: def __init__(self, price): self._price = price @property def price(self): return self._price @price.setter def price(self, new_price): if new_price > 0 and isinstance(new_price, float): self._price = new_price else: print("Please enter a valid price") @price.deleter def price(self): del self._price

Plus précisément, vous pouvez définir trois méthodes pour une propriété:

  • Un getter - pour accéder à la valeur de l'attribut.
  • Un setter - pour définir la valeur de l'attribut.
  • Un deleter - pour supprimer l'attribut d'instance.

Le prix est maintenant "Protégé"

Please note that the price attribute is now considered "protected" because we added a leading underscore to its name in self._price:

self._price = price

In Python, by convention, when you add a leading underscore to a name, you are telling other developers that it should not be accessed or modified directly outside of the class. It should only be accessed through intermediaries (getters and setters) if they are available.

🔸 Getter

Here we have the getter method:

@property def price(self): return self._price

Notice the syntax:

  • @property - Used to indicate that we are going to define a property. Notice how this immediately improves readability because we can clearly see the purpose of this method.
  • def price(self) - The header. Notice how the getter is named exactly like the property that we are defining: price. This is the name that we will use to access and modify the attribute outside of the class. The method only takes one formal parameter, self, which is a reference to the instance.
  • return self._price - This line is exactly what you would expect in a regular getter. The value of the protected attribute is returned.

Here is an example of the use of the getter method:

>>> house = House(50000.0) # Create instance >>> house.price # Access value 50000.0

Notice how we access the price attribute as if it were a public attribute. We are not changing the syntax at all, but we are actually using the getter as an intermediary to avoid accessing the data directly.

🔹 Setter

Now we have the setter method:

@price.setter def price(self, new_price): if new_price > 0 and isinstance(new_price, float): self._price = new_price else: print("Please enter a valid price")

Notice the syntax:

  • @price.setter - Used to indicate that this is the setter method for the price property. Notice that we are not using @property.setter, we are using @price.setter. The name of the property is included before .setter.
  • def price(self, new_price): - The header and the list of parameters. Notice how the name of the property is used as the name of the setter. We also have a second formal parameter (new_price), which is the new value that will be assigned to the price attribute (if it is valid).
  • Finally, we have the body of the setter where we validate the argument to check if it is a positive float and then, if the argument is valid, we update the value of the attribute. If the value is not valid, a descriptive message is printed. You can choose how to handle invalid values according the needs of your program.

This is an example of the use of the setter method with @property:

>>> house = House(50000.0) # Create instance >>> house.price = 45000.0 # Update value >>> house.price # Access value 45000.0

Notice how we are not changing the syntax, but now we are using an intermediary (the setter) to validate the argument before assigning it. The new value (45000.0) is passed as an argument to the setter :

house.price = 45000.0

If we try to assign an invalid value, we see the descriptive message. We can also check that the value was not updated:

>>> house = House(50000.0) >>> house.price = -50 Please enter a valid price >>> house.price 50000.0

💡 Tip: This proves that the setter method is working as an intermediary. It is being called "behind the scenes" when we try to update the value, so the descriptive message is displayed when the value is not valid.

🔸 Deleter

Finally, we have the deleter method:

@price.deleter def price(self): del self._price

Notice the syntax:

  • @price.deleter - Used to indicate that this is the deleter method for the price property. Notice that this line is very similar to @price.setter, but now we are defining the deleter method, so we write @price.deleter.
  • def price(self): - The header. This method only has one formal parameter defined, self.
  • del self._price - The body, where we delete the instance attribute.

💡 Tip: Notice that the name of the property is "reused" for all three methods.

This is an example of the use of the deleter method with @property:

# Create instance >>> house = House(50000.0) # The instance attribute exists >>> house.price 50000.0 # Delete the instance attribute >>> del house.price # The instance attribute doesn't exist >>> house.price Traceback (most recent call last): File "", line 1, in  house.price File "", line 8, in price return self._price AttributeError: 'House' object has no attribute '_price'

The instance attribute was deleted successfully ?. When we try to access it again, an error is thrown because the attribute doesn't exist anymore.

🔹 Some final Tips

You don't necessarily have to define all three methods for every property. You can define read-only properties by only including a getter method. You could also choose to define a getter and setter without a deleter.

If you think that an attribute should only be set when the instance is created or that it should only be modified internally within the class, you can omit the setter.

You can choose which methods to include depending on the context that you are working with.

🔸 In Summary

  • You can define properties with the @property syntax, which is more compact and readable.
  • @property can be considered the "pythonic" way of defining getters, setters, and deleters.
  • By defining properties, you can change the internal implementation of a class without affecting the program, so you can add getters, setters, and deleters that act as intermediaries "behind the scenes" to avoid accessing or modifying the data directly.

I really hope you liked my article and found it helpful. To learn more about Properties and Object Oriented Programming in Python, check out my online course, which includes 6+ hours of video lectures, coding exercises, and mini projects.