Création d’attributs gérés avec property en Python

Vous souhaitez ajouter des traitements supplémentaires (par exemple, vérification ou validation de type) à l’obtention ou à la définition d’un attribut d’instance.

Une façon simple de personnaliser l’accès à un attribut est de le définir comme une “propriété”. Par exemple, ce code définit une propriété qui ajoute une simple vérification de type à un attribut:

class Personne:
    def __init__(self, prenom):
        self.prenom = prenom

    # fonction Getter
    @property
    def prenom(self):
        return self._prenom

    # fonction Setter
    @prenom.setter
    def prenom(self, valeur):
        if not isinstance(valeur, str):
            raise TypeError('Chaîne de caractères attendue')
        self._prenom = valeur

    # fonction Deleter (optionnelle)
    @prenom.deleter
    def prenom(self):
        raise AttributeError("Impossible de supprimer un attribut")

Dans le code précédent, il y a trois méthodes connexes, qui doivent toutes porter le même nom. La première méthode est une fonction getter, et établit prenom comme étant une propriété.

Les deux autres méthodes attachent des fonctions optionnelles de setter et de deleter à la propriété prenom.

Il est important de souligner que les décorateurs @prenom.setter et @prenom.deleter ne seront pas définis à moins que l’attribut prenom ait déjà été établi comme une propriété utilisant @property.

Une caractéristique critique d’une propriété est qu’elle ressemble à un attribut normal, mais l’accès déclenche automatiquement les méthodes getter, setter et deleter. Par exemple:

>>> a = Personne('Alain')
>>> a.prenom       # appel du getter
'Alain'
>>> a.prenom = 222  # appel du setter
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "prop.py", line 14, in prenom
    raise TypeError('Chaîne de caractères attendue')
TypeError: Chaîne de caractères attendue
>>> del a.prenom
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: Impossible de supprimer un attribut
>>>

Lors de la mise en œuvre d’une propriété, les données sous-jacentes (le cas échéant) doivent encore être stockées quelque part. Ainsi, dans les méthodes get et set, vous voyez la manipulation directe d’un attribut _prenom, qui est l’emplacement des données réelles.

De plus, vous pouvez demander pourquoi la méthode __init__() définit self.prenom au lieu de self._prenom.

Dans cet exemple, le but de property est d’appliquer la vérification de type lors de la définition d’un attribut. Ainsi, il y a de fortes chances que vous souhaitiez également qu’une telle vérification ait lieu pendant l’initialisation.

En définissant self.prenom, l’opération set utilise la méthode setter (par opposition à la contourner en accédant à self._prenom).
Les propriétés peuvent également être définies pour les méthodes get et set existantes. Par exemple:

class Personne:
    def __init__(self, prenom):
        self.set_prenom(prenom)

    # Getter
    def get_prenom(self):
        return self._prenom

    # Setter
    def set_prenom(self, valeur):
        if not isinstance(valeur, str):
            raise TypeError('Chaîne de caractères attendue')
        self._prenom = valeur

    # Deleter (optionelle)
    def del_prenom(self):
        raise AttributeError("Impossible de supprimer un attribut")

    # Créer une propriété à partir de méthodes get/set existantes
    prenom = property(get_prenom, set_prenom, del_prenom)

Un attribut property est en fait un ensemble de méthodes regroupées. Si vous inspectez une classe avec property, vous pouvez trouver les méthodes brutes dans les attributs fget, fset et fdel de la propriété elle-même. Par exemple:

>>> Personne.prenom.fget
<function Personne.prenom at 0x1006a60e0>
>>> Personne.prenom.fset
<function Personne.prenom at 0x1006a6170>
>>> Personne.prenom.fdel
<function Personne.prenom at 0x1006a62e0>
>>>

Normalement, vous ne devriez pas appeler fget ou fset directement, mais ils sont déclenchés automatiquement lorsque vous accédez à la propriété.

Les propriétés ne doivent être utilisées que dans les cas où vous avez réellement besoin d’effectuer un traitement supplémentaire sur l’accès aux attributs.

Parfois, les programmeurs venant de langages tels que Java pensent que tous les accès devraient être gérés par les getters et les setters, et qu’ils devraient écrire du code comme ceci:

class Personne:
    def __init__(self, prenom):
        self.prenom = name
    @property
    def prenom(self):
        return self._prenom
    @prenom.setter
    def prenom(self, valeur):
        self._prenom = valeur

N’écrivez pas de propriétés qui n’ajoutent rien de plus comme ceci. D’une part, cela rend votre code plus compliqué et déroutant pour les autres. Deuxièmement, cela rendra votre programme beaucoup plus lent.

Enfin, il n’offre aucun avantage réel en termes de design. Plus précisément, si vous décidez plus tard que des traitements supplémentaires doivent être ajoutés à la gestion d’un attribut ordinaire, vous pouvez le promouvoir dans une propriété sans changer le code existant.

C’est parce que la syntaxe du code qui a accédé à l’attribut resterait inchangée.

Les propriétés peuvent aussi être un moyen de définir des attributs calculés. Ce sont des attributs qui ne sont pas réellement stockés, mais calculés à la demande. Par exemple:

import math
class Cercle:
    def __init__(self, rayon):
        self.rayon = rayon
    @property
    def surface(self):
        return math.pi * self.rayon ** 2
    @property
    def perimetre(self):
        return 2 * math.pi * self.rayon

Ici, l’utilisation de propriétés résulte en une interface d’instance très uniforme dans laquelle le rayon, la surface et le périmètre sont tous accessibles en tant qu’attributs simples, par opposition à un mélange d’attributs simples et d’appels de méthodes. Par exemple:

>>> c = Cercle(4.0)
>>> c.rayon
4.0

>>> c.surface        # Remarquer l'effet de l'absence des ()
50.26548245743669

>>> c.perimetre      # Remarquer l'effet de l'absence des ()
25.132741228718345
>>>

Bien que les propriétés vous donnent une interface de programmation élégante, parfois vous pouvez vouloir utiliser directement les fonctions getter et setter. Par exemple:

>>> p = Persone('Alain')
>>> p.get_prenom()
'Alain'
>>> p.set_prenom('John')
>>>

Cela se produit souvent dans des situations où le code Python est intégré dans une infrastructure plus large de systèmes ou de programmes.

Par exemple, une classe Python va peut-être être branchée dans un grand système distribué basé sur des appels de procédure distants ou des objets distribués.

Dans un tel contexte, il peut être beaucoup plus facile de travailler avec une méthode get/set explicite (comme un appel de méthode normal) plutôt qu’avec une propriété qui fait implicitement de tels appels.

Enfin, n’écrivez pas de code Python qui comporte beaucoup de définitions de propriétés répétitives. Par exemple:

class Personne:
    def __init__(self, prenom, nom):
        self.prenom = prenom
        self.nom = nom

    @property
    def prenom(self):
        return self._prenom

    @prenom.setter
    def prenom(self, valeur):
        if not isinstance(valeur, str):
            raise TypeError('chaîne de carcatères attendue')
        self._prenom = valeur

    # Code de property répété, mais pour un nom différent (mauvais !)
    @property
    def nom(self):
        return self._nom

    @nom.setter
    def nom(self, valeur):
        if not isinstance(valeur, str):
            raise TypeError('Expected a string')
        self._nom = valeur

La répétition du code conduit à un code gonflé, sujet aux erreurs et laid. Il s’avère qu’il existe de bien meilleures façons d’obtenir la même chose à l’aide de descripteurs ou de fermetures.

LAISSER UN COMMENTAIRE

Please enter your comment!
Please enter your name here