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)

1258voto

Martijn Pieters Points 271458

La fonction property() renvoie un objet descripteur spécial:

>>> property()

C'est cet objet qui possède des méthodes supplémentaires :

>>> property().getter

>>> property().setter

>>> property().deleter

Ces méthodes agissent également comme des décorateurs. Elles renvoient un nouvel objet de propriété :

>>> property().getter(None)

qui est une copie de l'ancien objet, mais avec l'une des fonctions remplacée.

N'oubliez pas que la syntaxe @decorator est juste du sucre syntaxique; la syntaxe :

@property
def foo(self): return self._foo

signifie vraiment la même chose que

def foo(self): return self._foo
foo = property(foo)

ainsi, la fonction foo est remplacée par property(foo), qui, comme nous l'avons vu ci-dessus, est un objet spécial. Ensuite, lorsque vous utilisez @foo.setter(), ce que vous faites est d'appeler la méthode property().setter que je vous ai montrée ci-dessus, qui renvoie une nouvelle copie de la propriété, mais cette fois avec la fonction setter remplacée par la méthode décorée.

La séquence suivante crée également une véritable propriété, en utilisant ces méthodes décoratives.

En premier lieu, nous créons quelques fonctions et un objet property avec seulement un getter:

>>> def getter(self): print('Get!')
... 
>>> def setter(self, value): print('Set to {!r}!'.format(value))
... 
>>> def deleter(self): print('Delete!')
... 
>>> prop = property(getter)
>>> prop.fget is getter
True
>>> prop.fset is None
True
>>> prop.fdel is None
True

Ensuite, nous utilisons la méthode .setter() pour ajouter un setter:

>>> prop = prop.setter(setter)
>>> prop.fget is getter
True
>>> prop.fset is setter
True
>>> prop.fdel is None
True

Enfin, nous ajoutons un deleter avec la méthode .deleter():

>>> prop = prop.deleter(deleter)
>>> prop.fget is getter
True
>>> prop.fset is setter
True
>>> prop.fdel is deleter
True

Enfin, l'objet property agit en tant qu'objet descripteur, donc il possède les méthodes .__get__(), .__set__() et .__delete__() pour se connecter à la récupération, la définition et la suppression d'attributs d'instance :

>>> class Foo: pass
... 
>>> prop.__get__(Foo(), Foo)
Get!
>>> prop.__set__(Foo(), 'bar')
Set to 'bar'!
>>> prop.__delete__(Foo())
Delete!

Le Guide Howto des descripteurs inclut une implémentation d'exemple en Python pur du type property():

class Property:
    "Emuler PyProperty_Type() dans Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("attribut illisible")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("impossible de définir l'attribut")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("impossible de supprimer l'attribut")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

14 votes

Très bien. Vous pourriez ajouter le fait qu'après Foo.prop = prop vous pouvez faire Foo().prop = 5; pront Foo().prop; del Foo().prop avec le résultat souhaité.

0 votes

Pourquoi property().getter et property().deleter ont la même adresse, mais property().setter ne l'a pas?

14 votes

Les objets de méthode sont créés à la volée et peuvent réutiliser la même adresse mémoire si disponible.

343voto

J0HN Points 10486

La documentation dit que c'est juste un raccourci pour créer des propriétés en lecture seule. Donc

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

est équivalent à

def getx(self):
    return self._x
x = property(getx)

73 votes

Le contexte complet (réponse la plus appréciée) est bon, mais cette réponse était pratiquement utile pour comprendre pourquoi quelqu'un d'autre avait utilisé @property comme décorateur dans leur classe.

6 votes

"...raccourci pour créer des propriétés en lecture seule." La réponse à un million de dollars!

0 votes

Il ne crée pas une propriété en lecture seule. Il crée une méthode getter "standard". Cette déclaration fonctionnera toujours comme prévu : obj.x = 5

177voto

AlexG Points 3695

Voici un exemple minimal de la façon dont @property peut être implémenté:

class Thing:
    def __init__(self, my_word):
        self._word = my_word 
    @property
    def word(self):
        return self._word

>>> print( Thing('ok').word )
'ok'

Sinon, word reste une méthode plutôt qu'une propriété.

class Thing:
    def __init__(self, my_word):
        self._word = my_word
    def word(self):
        return self._word

>>> print( Thing('ok').word() )
'ok'

2 votes

Comment cet exemple apparaîtrait-il si la fonction/propriété word() devait être définie dans init ?

20 votes

Quelqu'un peut-il s'il vous plaît expliquer pourquoi je créerais un décorateur de propriété ici, au lieu d'avoir simplement self.word = my_word -- ce qui fonctionnerait ensuite de la même manière print( Thing('ok').word ) = 'ok'

2 votes

@SilverSlash C'est juste un exemple simple, un cas d'utilisation réel impliquerait une méthode plus compliquée

113voto

Cleb Points 9410

Voici un autre exemple de comment @property peut aider lorsqu'il s'agit de retravailler du code qui est extrait de ici (je le résume uniquement ci-dessous) :

Imaginez que vous avez créé une classe Money comme ceci :

class Money:
    def __init__(self, dollars, cents):
        self.dollars = dollars
        self.cents = cents

et un utilisateur crée une bibliothèque dépendant de cette classe où il/elle utilise par exemple

money = Money(27, 12)

print("J'ai {} dollar et {} cents.".format(money.dollars, money.cents))
# affiche J'ai 27 dollar et 12 cents.

Supposons maintenant que vous décidiez de changer votre classe Money et de vous débarrasser des attributs dollars et cents mais de suivre uniquement le montant total de cents :

class Money:
    def __init__(self, dollars, cents):
        self.total_cents = dollars * 100 + cents

Si l'utilisateur mentionné ci-dessus essaie maintenant d'exécuter sa bibliothèque comme avant

money = Money(27, 12)

print("J'ai {} dollar et {} cents.".format(money.dollars, money.cents))

cela entraînera une erreur

AttributeError: l'objet 'Money' n'a pas d'attribut 'dollars'

Cela signifie que maintenant tout le monde qui dépend de votre classe Money originale devrait changer toutes les lignes de code où dollars et cents sont utilisés, ce qui peut être très douloureux... Comment cela pourrait-il être évité ? En utilisant @property !

Voilà comment :

class Money:
    def __init__(self, dollars, cents):
        self.total_cents = dollars * 100 + cents

    # Getter et setter pour dollars...
    @property
    def dollars(self):
        return self.total_cents // 100

    @dollars.setter
    def dollars(self, new_dollars):
        self.total_cents = 100 * new_dollars + self.cents

    # Et le getter et setter pour cents.
    @property
    def cents(self):
        return self.total_cents % 100

    @cents.setter
    def cents(self, new_cents):
        self.total_cents = 100 * self.dollars + new_cents

lorsque nous appelons maintenant depuis notre bibliothèque

money = Money(27, 12)

print("J'ai {} dollar et {} cents.".format(money.dollars, money.cents))
# affiche J'ai 27 dollar et 12 cents.

cela fonctionnera comme prévu et nous n'aurions pas eu à changer une seule ligne de code dans notre bibliothèque ! En fait, nous n'aurions même pas eu à savoir que la bibliothèque sur laquelle nous dépendons a changé.

Le setter fonctionne également correctement :

money.dollars += 2
print("J'ai {} dollar et {} cents.".format(money.dollars, money.cents))
# affiche J'ai 29 dollar et 12 cents.

money.cents += 10
print("J'ai {} dollar et {} cents.".format(money.dollars, money.cents))
# affiche J'ai 29 dollar et 22 cents.

Vous pouvez également utiliser @property dans des classes abstraites; je donne un exemple minimal ici.

6 votes

Votre résumé est très bon, l'exemple que ce site web prend est un peu étrange .. Un débutant se demanderait .. pourquoi ne pouvons-nous pas simplement nous en tenir à self.dollar = dollars? nous avons fait tellement avec @property, mais il semble qu'aucune fonctionnalité supplémentaire ne soit ajoutée.

6 votes

@ShengBi : Ne vous concentrez pas autant sur l'exemple concret, mais plus sur le principe sous-jacent : si, pour quelque raison que ce soit, vous devez refacturer du code, vous pouvez le faire sans affecter le code des autres.

3 votes

@cleb tu es le vrai MVP. Tout le monde utilise cet exemple de getter setter comme celui-ci, programiz.com/python-programming/property. Mais tu es le seul à expliquer pourquoi nous voulons une propriété. C'est parce que lorsque nous écrivons quelque chose sur laquelle beaucoup de personnes vont construire, nous voulons être capable de modifier les classes de base sans aucun impact réel sur la façon dont les successeurs utilisent ou construisent sur notre travail, en termes d'implémentation.

96voto

glglgl Points 35668

La première partie est simple :

@property
def x(self): ...

est la même que

def x(self): ...
x = property(x)
  • ce qui, à son tour, est la syntaxe simplifiée pour créer une property avec juste un getter.

La prochaine étape serait d'étendre cette propriété avec un setter et un supprimeur. Et cela se fait avec les méthodes appropriées :

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

retourne une nouvelle propriété qui hérite de tout de l'ancien x plus le setter donné.

x.deleter fonctionne de la même manière.

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