1320 votes

Comment fonctionne le décorateur @property en Python?

Je voudrais comprendre comment fonctionne la fonction intégrée property. Ce qui me perturbe, c'est que property peut également être utilisé comme un décorateur, mais il ne prend des arguments que lorsqu'il est utilisé comme une fonction intégré et non lorsqu'il est utilisé comme un décorateur.

Cet exemple provient de la documentation:

class C:
    def __init__(self):
        self._x = None

    def getx(self):
        return self._x
    def setx(self, value):
        self._x = value
    def delx(self):
        del self._x
    x = property(getx, setx, delx, "Je suis la propriété 'x'.")

Les arguments de property sont getx, setx, delx et une chaîne de documentation.

Dans le code ci-dessous, property est utilisé comme un décorateur. L'objet de celui-ci est la fonction x, mais dans le code ci-dessus, il n'y a pas de place pour une fonction objet dans les arguments.

class C:
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """Je suis la propriété 'x'."""
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        del self._x

Comment sont créés les décorateurs x.setter et x.deleter dans ce cas ?

28 votes

6 votes

property est en fait une classe (pas une fonction), bien qu'elle appelle probablement la méthode __init__() lorsque vous créez un objet, bien sûr. Utiliser help(property) depuis le terminal est instructif. help est également une classe pour une raison quelconque.

4 votes

Je pense que ce lien offre un bon exemple : [propriété] (journaldev.com/14893/python-property-decorator)

67voto

Bill Moore Points 1870

Ceci qui suit :

class C(object):
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """Je suis la propriété 'x'."""
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        del self._x

Est équivalent à :

class C(object):
    def __init__(self):
        self._x = None

    def _x_get(self):
        return self._x

    def _x_set(self, value):
        self._x = value

    def _x_del(self):
        del self._x

    x = property(_x_get, _x_set, _x_del, 
                    "Je suis la propriété 'x'.")

Est équivalent à :

class C(object):
    def __init__(self):
        self._x = None

    def _x_get(self):
        return self._x

    def _x_set(self, value):
        self._x = value

    def _x_del(self):
        del self._x

    x = property(_x_get, doc="Je suis la propriété 'x'.")
    x = x.setter(_x_set)
    x = x.deleter(_x_del)

Est équivalent à :

class C(object):
    def __init__(self):
        self._x = None

    def _x_get(self):
        return self._x
    x = property(_x_get, doc="Je suis la propriété 'x'.")

    def _x_set(self, value):
        self._x = value
    x = x.setter(_x_set)

    def _x_del(self):
        del self._x
    x = x.deleter(_x_del)

Qui est équivalent à :

class C(object):
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """Je suis la propriété 'x'."""
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        del self._x

6 votes

Les premiers et derniers exemples de code sont les mêmes (verbatim).

4 votes

Je pense que c'est intentionnel. En tout cas, c'était pour moi l'exemple le plus utile car je peux comprendre le sens de ces exemples. Merci @Bill Moore

46voto

Divyanshu Rawat Points 383

Commençons par les décorateurs Python.

Un décorateur Python est une fonction qui aide à ajouter des fonctionnalités supplémentaires à une fonction déjà définie.

En Python, tout est un objet. Les fonctions en Python sont des objets de première classe, ce qui signifie qu'elles peuvent être référencées par une variable, ajoutées dans des listes, passées en tant qu'arguments à une autre fonction, etc.

Considérez le code suivant.

def decorator_func(fun):
    def wrapper_func():
        print("La fonction Wrapper a démarré")
        fun()
        print("Fonction donnée décorée")
        # La fonction Wrapper ajoute quelque chose à la fonction passée et le décorateur 
        # renvoie la fonction Wrapper
    return wrapper_func

def dire_au_revoir():
    print("Au revoir!!")

dire_au_revoir = decorator_func(dire_au_revoir)
dire_au_revoir()

# Sortie :
#  La fonction Wrapper a démarré
#  Au revoir!!
#  Fonction donnée décorée

Ici, nous pouvons dire que la fonction décoratrice a modifié notre fonction dire_au_revoir et a ajouté quelques lignes de code supplémentaires.

Syntaxe Python pour décorateur

def decorator_func(fun):
    def wrapper_func():
        print("La fonction Wrapper a démarré")
        fun()
        print("Fonction donnée décorée")
        # La fonction Wrapper ajoute quelque chose à la fonction passée et le décorateur 
        # renvoie la fonction Wrapper
    return wrapper_func

@decorator_func
def dire_au_revoir():
    print("Au revoir!!")

dire_au_revoir()

Passons par tout cela avec un scénario. Mais avant cela, parlons de quelques principes de POO.

Les accesseurs et les mutateurs sont utilisés dans de nombreux langages de programmation orientée objet pour garantir le principe de l'encapsulation des données (qui est considéré comme l'intégration de données avec les méthodes qui opèrent sur ces données.)

Ces méthodes sont, bien sûr, l'accesseur pour récupérer les données et le mutateur pour changer les données.

Conformément à ce principe, les attributs d'une classe sont rendus privés pour les cacher et les protéger des autres codes.

Yup, @property est essentiellement un mode pythonique d'utiliser des accesseurs et des mutateurs.

Python a un excellent concept appelé propriété qui rend la vie d'un programmeur orienté objet beaucoup plus simple.

Supposons que vous décidiez de créer une classe qui pourrait stocker la température en degrés Celsius.

class Celsius:
    def __init__(self, temperature = 0):
        self.set_temperature(temperature)

    def to_fahrenheit(self):
        return (self.get_temperature() * 1.8) + 32

    def get_temperature(self):
        return self._temperature

    def set_temperature(self, value):
        if value < -273:
            raise ValueError("Température inférieure à -273 n'est pas possible")
        self._temperature = value

Code refactorisé, voici comment nous aurions pu le réaliser avec 'property.'

En Python, property() est une fonction intégrée qui crée et renvoie un objet de propriété.

Un objet de propriété a trois méthodes, getter(), setter() et delete().

class Celsius:
    def __init__(self, temperature = 0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

    def get_temperature(self):
        print("Obtenir la valeur")
        return self.temperature

    def set_temperature(self, value):
        if value < -273:
            raise ValueError("Température inférieure à -273 n'est pas possible")
        print("Définir la valeur")
        self.temperature = value

temperature = property(get_temperature,set_temperature)

Ici,

temperature = property(get_temperature,set_temperature)

aurait pu être décomposé comme suit,

# créer une propriété vide
temperature = property()
# assigner fget
temperature = temperature.getter(get_temperature)
# assigner fset
temperature = temperature.setter(set_temperature)

Point à noter :

  • get_temperature reste une propriété au lieu d'une méthode.

Maintenant, vous pouvez accéder à la valeur de température en écrivant.

C = Celsius()
C.temperature
# au lieu d'écrire C.get_temperature()

Nous pouvons aller plus loin et ne pas définir les noms get_temperature et set_temperature car ils sont inutiles et polluent l'espace de noms de la classe.

La manière pythonique de traiter le problème ci-dessus est d'utiliser @property.

class Celsius:
    def __init__(self, temperature = 0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

    @property
    def temperature(self):
        print("Obtenir la valeur")
        return self.temperature

    @temperature.setter
    def temperature(self, value):
        if value < -273:
            raise ValueError("Température inférieure à -273 n'est pas possible")
        print("Définir la valeur")
        self.temperature = value

Points à noter -

  1. Une méthode utilisée pour obtenir une valeur est décorée avec "@property".
  2. La méthode qui doit fonctionner comme le mutateur est décorée avec "@temperature.setter", Si la fonction avait été appelée "x", nous aurions dû la décorer avec "@x.setter".
  3. Nous avons écrit "deux" méthodes avec le même nom et un nombre différent de paramètres, "def temperature(self)" et "def temperature(self,x)".

Comme vous pouvez le voir, le code est certainement moins élégant.

Maintenant, parlons d'un scénario pratique de la vie réelle.

Disons que vous avez conçu une classe comme suit :

class NotreClasse:

    def __init__(self, a):
        self.x = a

y = NotreClasse(10)
print(y.x)

Supposons maintenant que notre classe soit devenue populaire auprès des clients et qu'ils ont commencé à l'utiliser dans leurs programmes, ils ont effectué toutes sortes d'affectations à l'objet.

Et un jour fatidique, un client de confiance est venu nous voir et a suggéré que "x" doit être une valeur entre 0 et 1000; c'est vraiment un scénario horrible!

Grâce aux propriétés, c'est facile : nous créons une version de propriété de "x".

class NotreClasse:

    def __init__(self,x):
        self.x = x

    @property
    def x(self):
        return self.__x

    @x.setter
    def x(self, x):
        if x < 0:
            self.__x = 0
        elif x > 1000:
            self.__x = 1000
        else:
            self.__x = x

C'est génial, n'est-ce pas : Vous pouvez commencer avec l'implémentation la plus simple imaginable, et vous êtes libre de migrer plus tard vers une version de propriété sans avoir à changer l'interface! Donc les propriétés ne sont pas juste un remplacement pour les accesseurs et les mutateurs!

Vous pouvez vérifier cette implémentation ici

7 votes

Votre classe Celsius va récursivement infiniment lors de la définition (ce qui signifie lors de l'instanciation).

1 votes

@Ted Petrou Je ne vous comprends pas? Comment cela va-t-il se répéter indéfiniment lors du réglage?

0 votes

Cela n'est en fait pas clair ... les gens se demandent pourquoi, mais l'exemple n'est pas convaincant...

31voto

Leo Skhrnkv Points 497

J'ai lu tous les messages ici et réalisé que nous pourrions avoir besoin d'un exemple de la vie réelle. Pourquoi, en fait, avons-nous @property? Donc, considérez une application Flask où vous utilisez un système d'authentification. Vous déclarez un modèle User dans models.py:

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(64), unique=True, index=True)
    username = db.Column(db.String(64), unique=True, index=True)
    password_hash = db.Column(db.String(128))

    ...

    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')

    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

Dans ce code, nous avons "caché" l'attribut password en utilisant @property ce qui déclenche une assertion AttributeError lorsque vous essayez d'y accéder directement, alors que nous avons utilisé @property.setter pour définir la variable d'instance réelle password_hash.

Maintenant dans auth/views.py nous pouvons instancier un utilisateur avec:

...
@auth.route('/register', methods=['GET', 'POST'])
def register():
    form = RegisterForm()
    if form.validate_on_submit():
        user = User(email=form.email.data,
                    username=form.username.data,
                    password=form.password.data)
        db.session.add(user)
        db.session.commit()
...

Remarquez l'attribut password qui provient d'un formulaire d'inscription lorsque l'utilisateur remplit le formulaire. La confirmation du mot de passe se fait côté front-end avec EqualTo('password', message='Passwords must match') (au cas où vous vous poseriez la question, mais c'est un sujet différent lié aux formulaires Flask).

J'espère que cet exemple sera utile

20voto

Devendra Bhat Points 495

Ce point a été clarifié par de nombreuses personnes là-haut mais voici un point direct que je recherchais. C'est ce que je pense être important pour commencer avec le décorateur @property. par exemple :

class UtilityMixin():
    @property
    def get_config(self):
        return "Il s'agit d'une propriété"

L'appel de la fonction "get_config()" fonctionnera comme ceci.

util = UtilityMixin()
print(util.get_config)

Si vous remarquez, je n'ai pas utilisé de crochets "()" pour appeler la fonction. C'est la chose fondamentale que je cherchais pour le décorateur @property. Ainsi, vous pouvez utiliser votre fonction comme si c'était une variable.

16voto

Victor Wang Points 137

La meilleure explication peut être trouvée ici : Python @Property Expliqué – Comment Utiliser et Quand ? (Exemples Complets) par Selva Prabhakaran | Publié le 5 novembre 2018

Cela m'a aidé à comprendre POURQUOI pas seulement COMMENT.

https://www.machinelearningplus.com/python/python-property/

2 votes

C'est la meilleure source pour l'instant

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X