Quelle est la façon pythonique d'utiliser les getters et setters?

341

Je le fais comme:

def set_property(property,value):  
def get_property(property):  

ou

object.property = value  
value = object.property

Je suis nouveau sur Python, donc j'explore toujours la syntaxe, et j'aimerais avoir des conseils pour le faire.

Jorge Guberte
la source

Réponses:

684

Essayez ceci: Propriété Python

L'exemple de code est:

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

    @property
    def x(self):
        """I'm the 'x' property."""
        print("getter of x called")
        return self._x

    @x.setter
    def x(self, value):
        print("setter of x called")
        self._x = value

    @x.deleter
    def x(self):
        print("deleter of x called")
        del self._x


c = C()
c.x = 'foo'  # setter called
foo = c.x    # getter called
del c.x      # deleter called
Grissiom
la source
2
Le setter pour x est-il appelé dans l'initialiseur lors de l'instanciation de _x?
Casey
7
@Casey: Non. Les références à ._x(qui n'est pas une propriété, juste un attribut ordinaire) contournent le propertywrapping. Seules les références pour .xpasser par le property.
ShadowRanger
278

Quelle est la façon pythonique d'utiliser les getters et setters?

La manière "Pythonique" n'est pas d'utiliser des "getters" et des "setters", mais d'utiliser des attributs simples, comme le montre la question, et delpour les supprimer (mais les noms sont modifiés pour protéger les innocents ... intégrés):

value = 'something'

obj.attribute = value  
value = obj.attribute
del obj.attribute

Si plus tard, vous souhaitez modifier le paramètre et obtenir, vous pouvez le faire sans avoir à modifier le code utilisateur, en utilisant le propertydécorateur:

class Obj:
    """property demo"""
    #
    @property            # first decorate the getter method
    def attribute(self): # This getter method name is *the* name
        return self._attribute
    #
    @attribute.setter    # the property decorates with `.setter` now
    def attribute(self, value):   # name, e.g. "attribute", is the same
        self._attribute = value   # the "value" name isn't special
    #
    @attribute.deleter     # decorate with `.deleter`
    def attribute(self):   # again, the method name is the same
        del self._attribute

(Chaque décorateur utilise des copies et met à jour l'objet de propriété précédent, alors notez que vous devez utiliser le même nom pour chaque fonction / méthode définie, récupérée et supprimée.

Après avoir défini ce qui précède, le paramètre d'origine, l'obtention et la suppression du code sont les mêmes:

obj = Obj()
obj.attribute = value  
the_value = obj.attribute
del obj.attribute

Vous devez éviter cela:

def set_property(property,value):  
def get_property(property):  

Premièrement, ce qui précède ne fonctionne pas, car vous ne fournissez pas d'argument pour l'instance que la propriété serait définie sur (généralement self), ce qui serait:

class Obj:

    def set_property(self, property, value): # don't do this
        ...
    def get_property(self, property):        # don't do this either
        ...

Deuxièmement, cela fait double emploi avec l'objectif de deux méthodes spéciales, __setattr__et __getattr__.

Troisièmement, nous avons également les fonctions setattret getattrintégrées.

setattr(object, 'property_name', value)
getattr(object, 'property_name', default_value)  # default is optional

Le @propertydécorateur sert à créer des getters et des setters.

Par exemple, nous pouvons modifier le comportement du paramètre pour placer des restrictions sur la valeur définie:

class Protective(object):

    @property
    def protected_value(self):
        return self._protected_value

    @protected_value.setter
    def protected_value(self, value):
        if acceptable(value): # e.g. type or range check
            self._protected_value = value

En général, nous voulons éviter d'utiliser propertyet simplement utiliser des attributs directs.

C'est ce à quoi s'attendent les utilisateurs de Python. En suivant la règle de la moindre surprise, vous devriez essayer de donner à vos utilisateurs ce qu'ils attendent, sauf si vous avez une raison très convaincante du contraire.

Manifestation

Par exemple, supposons que nous voulions que l'attribut protégé de notre objet soit un entier compris entre 0 et 100 inclus, et empêcher sa suppression, avec des messages appropriés pour informer l'utilisateur de sa bonne utilisation:

class Protective(object):
    """protected property demo"""
    #
    def __init__(self, start_protected_value=0):
        self.protected_value = start_protected_value
    # 
    @property
    def protected_value(self):
        return self._protected_value
    #
    @protected_value.setter
    def protected_value(self, value):
        if value != int(value):
            raise TypeError("protected_value must be an integer")
        if 0 <= value <= 100:
            self._protected_value = int(value)
        else:
            raise ValueError("protected_value must be " +
                             "between 0 and 100 inclusive")
    #
    @protected_value.deleter
    def protected_value(self):
        raise AttributeError("do not delete, protected_value can be set to 0")

(Notez que __init__fait référence à self.protected_valuemais les méthodes de propriété font référence self._protected_value. C'est ainsi qui __init__utilise la propriété via l'API publique, garantissant qu'elle est "protégée".)

Et l'utilisation:

>>> p1 = Protective(3)
>>> p1.protected_value
3
>>> p1 = Protective(5.0)
>>> p1.protected_value
5
>>> p2 = Protective(-5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __init__
  File "<stdin>", line 15, in protected_value
ValueError: protectected_value must be between 0 and 100 inclusive
>>> p1.protected_value = 7.3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 17, in protected_value
TypeError: protected_value must be an integer
>>> p1.protected_value = 101
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 15, in protected_value
ValueError: protectected_value must be between 0 and 100 inclusive
>>> del p1.protected_value
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 18, in protected_value
AttributeError: do not delete, protected_value can be set to 0

Les noms importent-ils?

Oui, ils le font . .setteret .deleterfaire des copies de la propriété d'origine. Cela permet aux sous-classes de modifier correctement le comportement sans altérer le comportement du parent.

class Obj:
    """property demo"""
    #
    @property
    def get_only(self):
        return self._attribute
    #
    @get_only.setter
    def get_or_set(self, value):
        self._attribute = value
    #
    @get_or_set.deleter
    def get_set_or_delete(self):
        del self._attribute

Maintenant, pour que cela fonctionne, vous devez utiliser les noms respectifs:

obj = Obj()
# obj.get_only = 'value' # would error
obj.get_or_set = 'value'  
obj.get_set_or_delete = 'new value'
the_value = obj.get_only
del obj.get_set_or_delete
# del obj.get_or_set # would error

Je ne sais pas où cela serait utile, mais le cas d'utilisation est si vous voulez une propriété get, set et / ou delete-only. Il vaut probablement mieux s'en tenir à la même propriété sémantiquement portant le même nom.

Conclusion

Commencez avec des attributs simples.

Si vous avez besoin ultérieurement de fonctionnalités pour définir, obtenir et supprimer, vous pouvez l'ajouter avec le décorateur de propriétés.

Évitez les fonctions nommées set_...et get_...- c'est à cela que servent les propriétés.

Aaron Hall
la source
Outre la duplication des fonctionnalités disponibles, pourquoi éviter d'écrire vos propres setters et getters? Je comprends que ce n'est peut-être pas la manière Pythonique, mais y a-t-il des problèmes vraiment sérieux que l'on pourrait rencontrer autrement?
user1350191
4
Dans votre démo, la __init__méthode fait référence self.protected_valuemais le getter et les setters font référence self._protected_value. Pourriez-vous expliquer comment cela fonctionne? J'ai testé votre code et il fonctionne tel quel - ce n'est donc pas une faute de frappe.
codeforester
2
@codeforester J'espérais répondre dans ma réponse plus tôt, mais jusqu'à ce que je puisse, ce commentaire devrait suffire. J'espère que vous pouvez voir qu'il utilise la propriété via l'API publique, en s'assurant qu'elle est "protégée". Il ne serait pas logique de le "protéger" avec une propriété, puis d'utiliser l'api non public à la place dans le __init__cas?
Aaron Hall
2
Oui, @AaronHall l'a compris maintenant. Je ne savais pas que j'appelais en self.protected_value = start_protected_valuefait la fonction setter; Je pensais que c'était une mission.
codeforester
1
à mon humble avis, cela devrait être la réponse acceptée, si j'ai bien compris, python prend juste le point opposé par rapport à java, par exemple. Au lieu de tout rendre privé par défaut et d'écrire du code supplémentaire lorsqu'il est nécessaire publiquement en python, vous pouvez tout rendre public et ajouter de la confidentialité plus tard
idclev 463035818
27
In [1]: class test(object):
    def __init__(self):
        self.pants = 'pants'
    @property
    def p(self):
        return self.pants
    @p.setter
    def p(self, value):
        self.pants = value * 2
   ....: 
In [2]: t = test()
In [3]: t.p
Out[3]: 'pants'
In [4]: t.p = 10
In [5]: t.p
Out[5]: 20
Autoplectique
la source
17

Utiliser @propertyet @attribute.settervous aide non seulement à utiliser la méthode "pythonique" mais aussi à vérifier la validité des attributs à la fois lors de la création de l'objet et lors de sa modification.

class Person(object):
    def __init__(self, p_name=None):
        self.name = p_name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, new_name):
        if type(new_name) == str: #type checking for name property
            self._name = new_name
        else:
            raise Exception("Invalid value for name")

Par cela, vous «cachez» réellement l' _nameattribut des développeurs clients et effectuez également des vérifications sur le type de propriété de nom. Notez qu'en suivant cette approche même pendant l'initiation, le passeur est appelé. Alors:

p = Person(12)

Mènera à:

Exception: Invalid value for name

Mais:

>>>p = person('Mike')
>>>print(p.name)
Mike
>>>p.name = 'George'
>>>print(p.name)
George
>>>p.name = 2.3 # Causes an exception
Farzad Vertigo
la source
16

Découvrez le @propertydécorateur .

Kevin Little
la source
34
C'est à peu près une réponse de lien uniquement.
Aaron Hall
6
@AaronHall: Ce n'est pas le cas, cependant. "Supprimez le balisage, et vous obtenez toujours au moins un peu d'informations utiles."
Jean-François Corbett
7
Comment est-ce une réponse complète? Un lien n'est pas une réponse.
Andy_A̷n̷d̷y̷
Je pense que c'est une bonne réponse, car la documentation y indique clairement comment l'utiliser (et restera à jour si l'implémentation de python change, et elle dirige l'OP vers la méthode que le répondeur suggère. @ Jean-FrançoisCorbett a clairement déclaré 'comment' c'est une réponse complète.
HunnyBear
Dans tous les cas, cette réponse n'ajoute rien d'utile aux autres réponses et devrait idéalement être supprimée.
Georgy
5

Vous pouvez utiliser des accesseurs / mutateurs (ie @attr.setteret @property) ou non, mais le plus important est d'être cohérent!

Si vous utilisez @propertypour accéder simplement à un attribut, par exemple

class myClass:
    def __init__(a):
        self._a = a

    @property
    def a(self):
        return self._a

utilisez-le pour accéder à chaque * attribut! Ce serait une mauvaise pratique d’accéder à certains attributs en utilisant @propertyet de laisser d’autres propriétés publiques (c.-à-d. Nom sans trait de soulignement) sans accesseur, par exemple ne pas faire

class myClass:
    def __init__(a, b):
        self.a = a
        self.b = b

    @property
    def a(self):
        return self.a

Notez qu'il self.bn'a pas d'accesseur explicite ici même s'il est public.

De même avec les setters (ou mutateurs ), n'hésitez pas à utiliser @attribute.settermais soyez cohérent! Quand vous faites par exemple

class myClass:
    def __init__(a, b):
        self.a = a
        self.b = b 

    @a.setter
    def a(self, value):
        return self.a = value

J'ai du mal à deviner votre intention. D'une part, vous dites que les deux aet bsont publics (pas de soulignement principal dans leurs noms), donc je devrais théoriquement être autorisé à accéder / muter (obtenir / définir) les deux. Mais ensuite, vous spécifiez un mutateur explicite uniquement pour a, ce qui me dit que je ne devrais peut-être pas pouvoir définir b. Puisque vous avez fourni un mutateur explicite, je ne sais pas si le manque d'accesseur explicite ( @property) signifie que je ne devrais pas être en mesure d'accéder à l'une de ces variables ou si vous étiez simplement économe en utilisant @property.

* L'exception est lorsque vous souhaitez explicitement rendre certaines variables accessibles ou modifiables mais pas les deux ou que vous souhaitez effectuer une logique supplémentaire lors de l'accès ou de la mutation d'un attribut. C'est à ce moment que j'utilise personnellement @propertyet @attribute.setter(sinon pas d'acesseurs / mutateurs explicites pour les attributs publics).

Enfin, les suggestions PEP8 et Google Style Guide:

PEP8, Designing for Inheritance dit:

Pour les attributs de données publiques simples, il est préférable d'exposer uniquement le nom de l'attribut, sans méthodes compliquées d'accesseur / mutateur . Gardez à l'esprit que Python fournit un chemin facile vers de futures améliorations, si vous constatez qu'un simple attribut de données doit développer un comportement fonctionnel. Dans ce cas, utilisez les propriétés pour masquer l'implémentation fonctionnelle derrière la syntaxe d'accès aux attributs de données simples.

D'autre part, selon les règles / propriétés du langage Python du Guide de style Google, la recommandation est de:

Utilisez les propriétés dans le nouveau code pour accéder aux données ou les définir là où vous auriez normalement utilisé des méthodes d'accesseur ou de définition simples et légères. Les propriétés doivent être créées avec le @propertydécorateur.

Les avantages de cette approche:

La lisibilité est augmentée en éliminant les appels explicites aux méthodes get et set pour un accès simple aux attributs. Permet aux calculs d'être paresseux. Considéré la manière Pythonic de maintenir l'interface d'une classe. En termes de performances, permettre aux propriétés de contourner des méthodes d'accesseur triviales lorsqu'un accès direct à une variable est raisonnable. Cela permet également d'ajouter des méthodes d'accesseur à l'avenir sans casser l'interface.

et contre:

Doit hériter de objectPython 2. Peut masquer les effets secondaires comme la surcharge de l'opérateur. Peut être déroutant pour les sous-classes.

Tomasz Bartkowiak
la source
1
Je suis fortement en désaccord. Si j'ai 15 attributs sur mon objet et que je veux en calculer un @property, utiliser le reste @propertysemble être une mauvaise décision.
Quelklef
D'accord, mais seulement s'il y a quelque chose de spécifique à propos de cet attribut particulier dont vous avez besoin @property(par exemple, exécuter une logique spéciale avant de retourner un attribut). Sinon, pourquoi décoreriez-vous un attribut @properyet pas d'autres?
Tomasz Bartkowiak
@Quelklef voir le sidenote dans le message (marqué d'un astérisque).
Tomasz Bartkowiak, le
Eh bien ... Si vous ne faites pas l'une des choses mentionnées par le sidenote, vous ne devriez pas utiliser @propertypour commencer, non? Si votre getter l'est return this._xet que votre setter l'est this._x = new_x, alors utiliser @propertyest un peu idiot.
Quelklef
1
Hmm, peut-être. Personnellement, je dirais que ce n'est pas bien --- c'est tout à fait superflu. Mais je peux voir d'où tu viens. Je suppose que je viens de lire votre article comme disant que "la chose la plus importante lors de l'utilisation @propertyest la cohérence".
Quelklef
-1

Vous pouvez utiliser les méthodes magiques __getattribute__et __setattr__.

class MyClass:
    def __init__(self, attrvalue):
        self.myattr = attrvalue
    def __getattribute__(self, attr):
        if attr == "myattr":
            #Getter for myattr
    def __setattr__(self, attr):
        if attr == "myattr":
            #Setter for myattr

Sachez que __getattr__et __getattribute__ne sont pas les mêmes. __getattr__n'est invoqué que lorsque l'attribut est introuvable.

Pika le magicien des baleines
la source