Utilisation de @property par rapport aux getters et setters


728

Voici une question de conception spécifique à Python:

class MyClass(object):
    ...
    def get_my_attr(self):
        ...

    def set_my_attr(self, value):
        ...

et

class MyClass(object):
    ...        
    @property
    def my_attr(self):
        ...

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

Python nous permet de le faire de toute façon. Si vous souhaitez concevoir un programme Python, quelle approche utiliseriez-vous et pourquoi?

Réponses:


614

Préférez les propriétés . C'est pour ça qu'ils sont là.

La raison en est que tous les attributs sont publics en Python. Le début des noms avec un ou deux traits de soulignement n'est qu'un avertissement que l'attribut donné est un détail d'implémentation qui pourrait ne pas rester le même dans les futures versions du code. Cela ne vous empêche pas d'obtenir ou de définir cet attribut. Par conséquent, l'accès aux attributs standard est la façon normale et pythonique d'accéder aux attributs.

L'avantage des propriétés est qu'elles sont syntaxiquement identiques à l'accès aux attributs, vous pouvez donc passer de l'une à l'autre sans modifier le code client. Vous pouvez même avoir une version d'une classe qui utilise des propriétés (par exemple, pour le code par contrat ou le débogage) et une autre qui ne l'est pas pour la production, sans changer le code qui l'utilise. Dans le même temps, vous n'avez pas besoin d'écrire des getters et setters pour tout, au cas où vous pourriez avoir besoin de mieux contrôler l'accès plus tard.


90
Les noms d'attribut avec un double soulignement sont gérés spécialement par Python; ce n'est pas seulement une simple convention. Voir docs.python.org/py3k/tutorial/classes.html#private-variables
6502

63
Ils sont traités différemment, mais cela ne vous empêche pas d'y accéder. PS: AD 30 C0
kindall

4
et parce que les caractères "@" sont moches en code python, et le déréférencement @decorators donne la même sensation que le code spaghetti.
Berry Tsakala

18
Je ne suis pas d'accord. Comment le code structuré est-il égal au code spaghetti? Python est un beau langage. Mais ce serait encore mieux avec un meilleur support pour des choses simples comme une bonne encapsulation et des classes structurées.

69
Bien que je sois d'accord dans la plupart des cas, faites attention à ne pas cacher les méthodes lentes derrière un décorateur @property. L'utilisateur de votre API s'attend à ce que l'accès aux propriétés fonctionne comme un accès variable, et s'éloigner trop de cette attente peut rendre votre API désagréable à utiliser.
defrex

154

En Python, vous n'utilisez pas de getters, de setters ou de propriétés juste pour le plaisir. Vous utilisez d'abord des attributs, puis plus tard, uniquement si nécessaire, migrez éventuellement vers une propriété sans avoir à modifier le code à l'aide de vos classes.

Il y a en effet beaucoup de code avec l'extension .py qui utilise des getters et setters et l'héritage et des classes inutiles partout où par exemple un simple tuple ferait l'affaire, mais c'est du code de personnes écrivant en C ++ ou Java en utilisant Python.

Ce n'est pas du code Python.


46
@ 6502, quand vous avez dit "[…] classes inutiles partout où par exemple un simple tuple ferait l'affaire": l'avantage d'une classe par rapport à un tuple, c'est qu'une instance de classe fournit des noms explicites pour accéder à ses parties, alors qu'un tuple ne le fait pas . Les noms sont plus lisibles et évitent les erreurs que la souscription des tuples, surtout quand cela doit être passé en dehors du module actuel.
Hibou57

15
@ Hibou57: Je ne dis pas que les cours sont inutiles. Mais parfois, un tuple est plus que suffisant. Le problème est cependant que ceux qui viennent de Java ou C ++ n'ont d'autre choix que de créer des classes pour tout parce que d'autres possibilités sont simplement ennuyeuses à utiliser dans ces langues. Un autre symptôme typique de la programmation Java / C ++ utilisant Python est la création de classes abstraites et de hiérarchies de classes complexes sans aucune raison pour laquelle en Python vous pouvez simplement utiliser des classes indépendantes grâce à la frappe de canard.
6502

39
@ Hibou57 pour cela, vous pouvez également utiliser namedtuple: doughellmann.com/PyMOTW/collections/namedtuple.html
hugo24

5
@JonathonReinhart: il EST dans la bibliothèque standard depuis 2.6 ... voir docs.python.org/2/library/collections.html
6502

1
Lorsque "éventuellement migrer vers une propriété si nécessaire", il est fort probable que vous cassiez du code en utilisant vos classes. Les propriétés introduisent souvent des restrictions - tout code qui ne s'attendait pas à ces restrictions sera rompu dès que vous les introduirez.
yaccob

118

L'utilisation des propriétés vous permet de commencer avec des accès d'attributs normaux, puis de les sauvegarder avec des getters et des setters par la suite si nécessaire .


3
@GregKrsak Cela semble étrange parce qu'il l'est. La "chose des adultes consentants" était un mème en python d'avant l'ajout des propriétés. C'était la réponse du stock aux gens qui se plaignaient du manque de modificateurs d'accès. Lorsque des propriétés ont été ajoutées, une encapsulation soudaine devient souhaitable. La même chose s'est produite avec les classes de base abstraites. "Python a toujours été en guerre avec la rupture de l'encapsulation. La liberté est l'esclavage. Lambdas ne devrait tenir que sur une seule ligne."
johncip

71

La réponse courte est: les propriétés gagnent haut la main . Toujours.

Il y a parfois un besoin de getters et de setters, mais même alors, je les "cache" au monde extérieur. Il y a beaucoup de façons de le faire en Python ( getattr, setattr, __getattribute__, etc ..., mais très concise et propre est:

def set_email(self, value):
    if '@' not in value:
        raise Exception("This doesn't look like an email address.")
    self._email = value

def get_email(self):
    return self._email

email = property(get_email, set_email)

Voici un bref article qui présente le sujet des getters et setters en Python.


1
@BasicWolf - Je pensais qu'il était implicitement clair que je suis du côté de la propriété! :) Mais j'ajoute un para à ma réponse pour clarifier cela.
mac

9
CONSEIL: Le mot "toujours" est un indice que l'auteur tente de vous convaincre avec une affirmation, pas un argument. Il en va de même pour la présence de caractères gras. (Je veux dire, si vous voyez CAPS à la place, alors - whoa - cela doit être vrai.) déclare que c'est mieux. En réalité, les propriétés violent la règle "Explicit is better than implicit", mais personne ne veut l'admettre. Il est entré dans le langage, donc maintenant il est déclaré "Pythonic" via un argument tautologique.
Stuart Berg

3
Aucun sentiment blessé. :-P J'essaie juste de souligner que les conventions "Pythonic" sont incohérentes dans ce cas: "Explicit vaut mieux qu'implicite" est en conflit direct avec l'utilisation de a property. (Il ressemble à une affectation simple, mais il appelle une fonction.) Par conséquent, "Pythonic" est essentiellement un terme dénué de sens, sauf par la définition tautologique: "Les conventions Pythonic sont des choses que nous avons définies comme étant Pythonic."
Stuart Berg

1
Maintenant, l' idée d'avoir un ensemble de conventions qui suivent un thème est géniale . Si un tel ensemble de conventions existait, alors vous pourriez l'utiliser comme un ensemble d'axiomes pour guider votre réflexion, pas simplement une longue liste de contrôle d'astuces à mémoriser, ce qui est nettement moins utile. Les axiomes pourraient être utilisés pour l' extrapolation et vous aider à aborder des problèmes que personne n'a encore vus. C'est dommage que la propertyfonctionnalité menace de rendre l'idée des axiomes pythoniques presque sans valeur. Il ne nous reste donc qu'une liste de contrôle.
Stuart Berg

1
Je ne suis pas d'accord. Je préfère les propriétés dans la plupart des situations, mais lorsque vous souhaitez souligner que la définition de quelque chose a des effets secondaires autres que la modification de l' selfobjet , des paramètres explicites peuvent être utiles. Par exemple, user.email = "..."il ne semble pas qu'il puisse déclencher une exception car il ressemble à la définition d'un attribut, alors user.set_email("...")qu'il est clair qu'il pourrait y avoir des effets secondaires comme des exceptions.
bluenote10

65

[ TL; DR? Vous pouvez passer à la fin pour un exemple de code .]

En fait, je préfère utiliser un idiome différent, qui est un peu impliqué pour une utilisation unique, mais c'est bien si vous avez un cas d'utilisation plus complexe.

Un peu de fond d'abord.

Les propriétés sont utiles en ce qu'elles nous permettent de gérer à la fois la définition et l'obtention de valeurs de manière programmatique, mais permettent toujours d'accéder aux attributs en tant qu'attributs. Nous pouvons transformer «obtient» en «calculs» (essentiellement) et nous pouvons transformer «ensembles» en «événements». Disons donc que nous avons la classe suivante, que j'ai codée avec des getters et setters de type Java.

class Example(object):
    def __init__(self, x=None, y=None):
        self.x = x
        self.y = y

    def getX(self):
        return self.x or self.defaultX()

    def getY(self):
        return self.y or self.defaultY()

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

    def setY(self, y):
        self.y = y

    def defaultX(self):
        return someDefaultComputationForX()

    def defaultY(self):
        return someDefaultComputationForY()

Vous vous demandez peut-être pourquoi je n'ai pas appelé defaultXet defaultYdans la __init__méthode de l'objet . La raison en est que dans notre cas, je veux supposer que les someDefaultComputationméthodes renvoient des valeurs qui varient dans le temps, par exemple un horodatage, et chaque fois que x(ou y) n'est pas défini (où, dans le cadre de cet exemple, «non défini» signifie «défini à Aucun ") Je veux la valeur du calcul par défaut de x(ou yde).

C'est donc boiteux pour un certain nombre de raisons décrites ci-dessus. Je vais le réécrire en utilisant les propriétés:

class Example(object):
    def __init__(self, x=None, y=None):
        self._x = x
        self._y = y

    @property
    def x(self):
        return self.x or self.defaultX()

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

    @property
    def y(self):
        return self.y or self.defaultY()

    @y.setter
    def y(self, value):
        self._y = value

    # default{XY} as before.

Qu'avons-nous gagné? Nous avons acquis la possibilité de faire référence à ces attributs en tant qu'attributs même si, en coulisse, nous finissons par exécuter des méthodes.

Bien sûr, le vrai pouvoir des propriétés est que nous voulons généralement que ces méthodes fassent quelque chose en plus de simplement obtenir et définir des valeurs (sinon cela ne sert à rien d'utiliser des propriétés). Je l'ai fait dans mon exemple getter. Nous exécutons essentiellement un corps de fonction pour récupérer une valeur par défaut chaque fois que la valeur n'est pas définie. Il s'agit d'un schéma très courant.

Mais que perdons-nous et que ne pouvons-nous pas faire?

Le principal inconvénient, à mon avis, est que si vous définissez un getter (comme nous le faisons ici), vous devez également définir un setter. [1] C'est un bruit supplémentaire qui encombre le code.

Un autre inconvénient est que nous devons encore initialiser les valeurs xet . (Eh bien, bien sûr, nous pourrions les ajouter en utilisant, mais c'est plus de code supplémentaire.)y__init__setattr()

Troisièmement, contrairement à l'exemple de type Java, les getters ne peuvent pas accepter d'autres paramètres. Maintenant, je vous entends déjà dire, eh bien, si cela prend des paramètres, ce n'est pas un getter! D'un point de vue officiel, c'est vrai. Mais dans un sens pratique, il n'y a aucune raison de ne pas pouvoir paramétrer un attribut nommé - comme x- et définir sa valeur pour certains paramètres spécifiques.

Ce serait bien si nous pouvions faire quelque chose comme:

e.x[a,b,c] = 10
e.x[d,e,f] = 20

par exemple. Le plus proche que nous pouvons obtenir est de remplacer l'affectation pour impliquer une sémantique spéciale:

e.x = [a,b,c,10]
e.x = [d,e,f,30]

et bien sûr, assurez-vous que notre setter sait comment extraire les trois premières valeurs en tant que clé d'un dictionnaire et définir sa valeur sur un nombre ou quelque chose.

Mais même si nous le faisions, nous ne pourrions toujours pas le supporter avec des propriétés car il n'y a aucun moyen d'obtenir la valeur car nous ne pouvons pas du tout passer de paramètres au getter. Nous avons donc dû tout retourner, introduisant une asymétrie.

Le getter / setter de style Java nous permet de gérer cela, mais nous sommes de retour à avoir besoin de getter / setters.

Dans mon esprit, ce que nous voulons vraiment, c'est quelque chose qui reflète les exigences suivantes:

  • Les utilisateurs définissent une seule méthode pour un attribut donné et peuvent y indiquer si l'attribut est en lecture seule ou en lecture-écriture. Les propriétés échouent à ce test si l'attribut est accessible en écriture.

  • Il n'est pas nécessaire que l'utilisateur définisse une variable supplémentaire sous-jacente à la fonction, nous n'avons donc pas besoin de __init__ou setattrdans le code. La variable existe simplement du fait que nous avons créé cet attribut de nouveau style.

  • Tout code par défaut pour l'attribut s'exécute dans le corps de la méthode lui-même.

  • Nous pouvons définir l'attribut comme attribut et le référencer comme attribut.

  • Nous pouvons paramétrer l'attribut.

En termes de code, nous voulons un moyen d'écrire:

def x(self, *args):
    return defaultX()

et pouvoir ensuite faire:

print e.x     -> The default at time T0
e.x = 1
print e.x     -> 1
e.x = None
print e.x     -> The default at time T1

et ainsi de suite.

Nous voulons également un moyen de le faire pour le cas spécial d'un attribut paramétrable, mais nous permettons toujours au cas d'affectation par défaut de fonctionner. Vous verrez comment j'ai abordé cela ci-dessous.

Maintenant au point (yay! Le point!). La solution que j'ai trouvée pour cela est la suivante.

Nous créons un nouvel objet pour remplacer la notion de propriété. L'objet est destiné à stocker la valeur d'une variable qui lui est définie, mais conserve également une poignée sur le code qui sait calculer une valeur par défaut. Son travail consiste à stocker l'ensemble valueou à exécuter le methodsi cette valeur n'est pas définie.

Appelons ça un UberProperty.

class UberProperty(object):

    def __init__(self, method):
        self.method = method
        self.value = None
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def clearValue(self):
        self.value = None
        self.isSet = False

Je suppose que methodc'est une méthode de classe, valuec'est la valeur de la UberProperty, et j'ai ajouté isSetparce que Nonepeut être une vraie valeur et cela nous permet une façon propre de déclarer qu'il n'y a vraiment "aucune valeur". Une autre façon est une sorte de sentinelle.

Cela nous donne essentiellement un objet qui peut faire ce que nous voulons, mais comment pouvons-nous le mettre sur notre classe? Eh bien, les propriétés utilisent des décorateurs; pourquoi pas nous? Voyons à quoi cela pourrait ressembler (à partir de maintenant, je vais m'en tenir à l'utilisation d'un seul «attribut», x).

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

Bien sûr, cela ne fonctionne pas encore. Nous devons implémenter uberPropertyet nous assurer qu'il gère à la fois les get et les sets.

Commençons par get.

Ma première tentative a été de simplement créer un nouvel objet UberProperty et de le renvoyer:

def uberProperty(f):
    return UberProperty(f)

J'ai rapidement découvert, bien sûr, que cela ne fonctionne pas: Python ne lie jamais l'appelable à l'objet et j'ai besoin de l'objet pour appeler la fonction. Même la création du décorateur dans la classe ne fonctionne pas, car bien que nous ayons maintenant la classe, nous n'avons toujours pas d'objet avec lequel travailler.

Nous allons donc devoir pouvoir faire plus ici. Nous savons qu'une méthode n'a besoin d'être représentée qu'une seule fois, alors allons-y et gardons notre décorateur, mais modifions UberPropertypour ne stocker que la methodréférence:

class UberProperty(object):

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

Ce n'est pas non plus appelable, donc pour le moment rien ne fonctionne.

Comment complétons-nous l'image? Eh bien, que finissons-nous quand nous créons l'exemple de classe en utilisant notre nouveau décorateur:

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

print Example.x     <__main__.UberProperty object at 0x10e1fb8d0>
print Example().x   <__main__.UberProperty object at 0x10e1fb8d0>

dans les deux cas, nous récupérons ce UberPropertyqui bien sûr n'est pas appelable, donc cela n'est pas très utile.

Ce dont nous avons besoin, c'est d'un moyen de lier dynamiquement l' UberPropertyinstance créée par le décorateur après la création de la classe à un objet de la classe avant que cet objet ne soit renvoyé à cet utilisateur pour utilisation. Ouais, c'est un __init__appel, mec.

Écrivons ce que nous voulons que notre résultat de recherche soit le premier. Nous lions un UberPropertyà une instance, donc une chose évidente à retourner serait un BoundUberProperty. C'est là que nous conserverons l'état de l' xattribut.

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

Maintenant, nous la représentation; comment les intégrer à un objet? Il existe quelques approches, mais la plus simple à expliquer utilise simplement la __init__méthode pour effectuer ce mappage. Au moment où __init__nous appelons nos décorateurs ont couru, il suffit donc de regarder à travers l'objet __dict__et de mettre à jour tous les attributs où la valeur de l'attribut est de type UberProperty.

Maintenant, les propriétés uber sont cool et nous voudrons probablement les utiliser beaucoup, il est donc logique de simplement créer une classe de base qui le fait pour toutes les sous-classes. Je pense que vous savez comment la classe de base sera appelée.

class UberObject(object):
    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)

Nous ajoutons ceci, changeons notre exemple pour hériter de UberObject, et ...

e = Example()
print e.x               -> <__main__.BoundUberProperty object at 0x104604c90>

Après avoir modifié xpour être:

@uberProperty
def x(self):
    return *datetime.datetime.now()*

Nous pouvons exécuter un test simple:

print e.x.getValue()
print e.x.getValue()
e.x.setValue(datetime.date(2013, 5, 31))
print e.x.getValue()
e.x.clearValue()
print e.x.getValue()

Et nous obtenons la sortie que nous voulions:

2013-05-31 00:05:13.985813
2013-05-31 00:05:13.986290
2013-05-31
2013-05-31 00:05:13.986310

(Gee, je travaille tard.)

Notez que je l' ai utilisé getValue, setValueet clearValueici. C'est parce que je n'ai pas encore lié les moyens de les renvoyer automatiquement.

Mais je pense que c'est un bon endroit pour s'arrêter pour l'instant, car je suis fatigué. Vous pouvez également voir que la fonctionnalité de base que nous voulions est en place; le reste est de la vitrine. Habillage de fenêtre de convivialité important, mais cela peut attendre jusqu'à ce que j'ai un changement pour mettre à jour le message.

Je terminerai l'exemple dans la prochaine publication en abordant ces choses:

  • Nous devons nous assurer que UberObject __init__est toujours appelé par des sous-classes.

    • Donc, soit nous le forçons à être appelé quelque part, soit nous l'empêchons d'être mis en œuvre.
    • Nous verrons comment faire cela avec une métaclasse.
  • Nous devons nous assurer que nous gérons le cas commun où quelqu'un «alias» une fonction pour autre chose, comme:

      class Example(object):
          @uberProperty
          def x(self):
              ...
    
          y = x
  • Nous devons e.xrevenir e.x.getValue()par défaut.

    • Ce que nous verrons réellement, c'est que c'est un domaine où le modèle échoue.
    • Il s'avère que nous aurons toujours besoin d'utiliser un appel de fonction pour obtenir la valeur.
    • Mais nous pouvons le faire ressembler à un appel de fonction normal et éviter d'avoir à l'utiliser e.x.getValue(). (Faire cela est évident, si vous ne l'avez pas déjà résolu.)
  • Nous devons soutenir la configuration e.x directly, comme dans e.x = <newvalue>. Nous pouvons également le faire dans la classe parente, mais nous devrons mettre à jour notre __init__code pour le gérer.

  • Enfin, nous ajouterons des attributs paramétrés. La façon dont nous procéderons également devrait être assez évidente.

Voici le code tel qu'il existe jusqu'à présent:

import datetime

class UberObject(object):
    def uberSetter(self, value):
        print 'setting'

    def uberGetter(self):
        return self

    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)


class UberProperty(object):
    def __init__(self, method):
        self.method = method

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

    def uberProperty(f):
        return UberProperty(f)

class Example(UberObject):

    @uberProperty
    def x(self):
        return datetime.datetime.now()

[1] Je suis peut-être en retard sur la question de savoir si c'est toujours le cas.


53
Oui, c'est «tldr». Pouvez-vous résumer ce que vous essayez de faire ici?
poolie

9
@Adam return self.x or self.defaultX()c'est un code dangereux. Que se passe-t-il quand self.x == 0?
Kelly Thomas

Pour info, vous pouvez faire en sorte que vous puissiez paramétrer le getter, en quelque sorte. Cela impliquerait de faire de la variable une classe personnalisée, dont vous avez remplacé la __getitem__méthode. Ce serait bizarre cependant, car vous auriez alors un python complètement non standard.
le

2
@KellyThomas J'essaie simplement de garder l'exemple simple. Pour le faire correctement, vous devez créer et supprimer complètement l'entrée x dict , car même une valeur None peut avoir été spécifiquement définie. Mais oui, vous avez absolument raison, c'est quelque chose que vous devez considérer dans un cas d'utilisation en production.
Adam Donahue

Les getters de type Java vous permettent de faire exactement le même calcul, n'est-ce pas?
cqfd

26

Je pense que les deux ont leur place. Un problème avec l'utilisation @propertyest qu'il est difficile d'étendre le comportement des getters ou setters dans les sous-classes en utilisant des mécanismes de classe standard. Le problème est que les fonctions getter / setter réelles sont cachées dans la propriété.

Vous pouvez réellement mettre la main sur les fonctions, par exemple avec

class C(object):
    _p = 1
    @property
    def p(self):
        return self._p
    @p.setter
    def p(self, val):
        self._p = val

vous pouvez accéder aux fonctions getter et setter au fur C.p.fgetet à mesure C.p.fset, mais vous ne pouvez pas facilement utiliser les fonctions d'héritage de méthode normales (par exemple super) pour les étendre. Après avoir creusé dans les subtilités du super, vous pouvez en effet utiliser le super de cette manière:

# Using super():
class D(C):
    # Cannot use super(D,D) here to define the property
    # since D is not yet defined in this scope.
    @property
    def p(self):
        return super(D,D).p.fget(self)

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for D'
        super(D,D).p.fset(self, val)

# Using a direct reference to C
class E(C):
    p = C.p

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for E'
        C.p.fset(self, val)

L'utilisation de super () est cependant assez maladroite, car la propriété doit être redéfinie, et vous devez utiliser le mécanisme super (cls, cls) légèrement contre-intuitif pour obtenir une copie non liée de p.


20

L'utilisation des propriétés est pour moi plus intuitive et s'intègre mieux dans la plupart des codes.

Comparant

o.x = 5
ox = o.x

contre.

o.setX(5)
ox = o.getX()

est pour moi assez évident qui est plus facile à lire. Les propriétés permettent également de rendre les variables privées beaucoup plus faciles.


12

Je préférerais ne pas utiliser ni l'un ni l'autre dans la plupart des cas. Le problème avec les propriétés est qu'elles rendent la classe moins transparente. Surtout, c'est un problème si vous leviez une exception à partir d'un setter. Par exemple, si vous avez une propriété Account.email:

class Account(object):
    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, value):
        if '@' not in value:
            raise ValueError('Invalid email address.')
        self._email = value

alors l'utilisateur de la classe ne s'attend pas à ce que l'attribution d'une valeur à la propriété puisse provoquer une exception:

a = Account()
a.email = 'badaddress'
--> ValueError: Invalid email address.

Par conséquent, l'exception peut ne pas être gérée et se propager trop haut dans la chaîne d'appels pour être traitée correctement, ou entraîner une traceback très inutile qui est présentée à l'utilisateur du programme (ce qui est malheureusement trop courant dans le monde de python et java). ).

Je voudrais également éviter d'utiliser des getters et setters:

  • parce que les définir à l'avance pour toutes les propriétés prend beaucoup de temps,
  • rend la quantité de code inutilement plus longue, ce qui rend la compréhension et la maintenance du code plus difficiles,
  • si vous les définissiez pour les propriétés uniquement selon les besoins, l'interface de la classe changerait, blessant tous les utilisateurs de la classe

Au lieu de propriétés et de getters / setters, je préfère faire la logique complexe dans des endroits bien définis, comme dans une méthode de validation:

class Account(object):
    ...
    def validate(self):
        if '@' not in self.email:
            raise ValueError('Invalid email address.')

ou une méthode Account.save similaire.

Notez que je n'essaie pas de dire qu'il n'y a pas de cas où les propriétés sont utiles, mais seulement que vous pourriez être mieux si vous pouvez rendre vos classes assez simples et transparentes pour que vous n'en ayez pas besoin.


3
@ user2239734 Je pense que vous comprenez mal le concept de propriétés. Bien que vous puissiez valider la valeur lors de la définition de la propriété, il n'est pas nécessaire de le faire. Vous pouvez avoir à la fois des propriétés et une validate()méthode dans une classe. Une propriété est simplement utilisée lorsque vous avez une logique complexe derrière une obj.x = yaffectation simple , et cela dépend de la nature de la logique.
Zaur Nasibov

12

J'ai l'impression que les propriétés consistent à vous permettre d'obtenir les frais généraux d'écriture des getters et setters uniquement lorsque vous en avez réellement besoin.

La culture de programmation Java conseille fortement de ne jamais donner accès aux propriétés, et de passer par des getters et setters, et uniquement ceux qui sont réellement nécessaires. Il est un peu bavard de toujours écrire ces morceaux de code évidents, et notez que 70% du temps ils ne sont jamais remplacés par une logique non triviale.

En Python, les gens se soucient réellement de ce type de surcharge, de sorte que vous pouvez adopter la pratique suivante:

  • Ne pas utiliser les getters et setters au début, quand ils ne sont pas nécessaires
  • Utilisez-les @propertypour les implémenter sans changer la syntaxe du reste de votre code.

1
"et notez que 70% du temps, ils ne sont jamais remplacés par une logique non triviale." - c'est un nombre assez spécifique, vient-il d'un endroit, ou est-ce que vous envisagez cela comme une sorte de "grande majorité" ondulée (je ne suis pas facétieux, s'il y a une étude qui quantifie ce nombre, je serais vraiment intéressé à le lire)
Adam Parkin

1
Oh non désolé. Il semble que j'aie une étude pour sauvegarder ce numéro, mais je ne le pensais que comme "la plupart du temps".
fulmicoton

7
Ce n'est pas que les gens se soucient de la surcharge, c'est qu'en Python, vous pouvez passer d'un accès direct à des méthodes d'accesseur sans changer le code client, vous n'avez donc rien à perdre en exposant directement les propriétés au début.
Neil G

10

Je suis surpris que personne n'ait mentionné que les propriétés sont des méthodes liées d'une classe de descripteurs, Adam Donohue et NeilenMarais ont exactement cette idée dans leurs messages - que les getters et setters sont des fonctions et peuvent être utilisés pour:

  • valider
  • modifier les données
  • type de canard (contraindre le type à un autre type)

Cela présente une puce moyen de masquer les détails de l'implémentation et le code brut comme l'expression régulière, les transtypages, essayez .. sauf les blocs, les assertions ou les valeurs calculées.

En général, faire CRUD sur un objet peut souvent être assez banal, mais considérons l'exemple des données qui seront conservées dans une base de données relationnelle. Les ORM peuvent masquer les détails d'implémentation de vernaculaires SQL particuliers dans les méthodes liées à fget, fset, fdel définies dans une classe de propriétés qui gérera les échelles if .. elif .. else si laides dans le code OO - exposant le simple et élégant self.variable = somethinget évite les détails pour le développeur utilisant l'ORM.

Si l'on ne considère les propriétés que comme un vestige morne d'un langage de Bondage et de Discipline (c'est-à-dire Java), il leur manque le point de descripteurs.


6

Dans les projets complexes, je préfère utiliser des propriétés en lecture seule (ou des getters) avec une fonction de définition explicite:

class MyClass(object):
...        
@property
def my_attr(self):
    ...

def set_my_attr(self, value):
    ...

Dans les projets de longue durée, le débogage et la refactorisation prennent plus de temps que l'écriture du code lui-même. Il y a plusieurs inconvénients à utiliser @property.setterqui rendent le débogage encore plus difficile:

1) python permet de créer de nouveaux attributs pour un objet existant. Il est donc très difficile de suivre une erreur d'impression suivante:

my_object.my_atttr = 4.

Si votre objet est un algorithme compliqué, vous passerez un certain temps à essayer de comprendre pourquoi il ne converge pas (remarquez un «t» supplémentaire dans la ligne ci-dessus)

2) setter peut parfois évoluer vers une méthode compliquée et lente (par exemple, toucher une base de données). Il serait assez difficile pour un autre développeur de comprendre pourquoi la fonction suivante est très lente. Il pourrait consacrer beaucoup de temps à la do_something()méthode de profilage , alors qu'il my_object.my_attr = 4.est en fait la cause du ralentissement:

def slow_function(my_object):
    my_object.my_attr = 4.
    my_object.do_something()

5

Les @propertygetters et setters traditionnels ont tous deux leurs avantages. Cela dépend de votre cas d'utilisation.

Les avantages de @property

  • Vous n'avez pas besoin de changer l'interface tout en modifiant l'implémentation de l'accès aux données. Lorsque votre projet est petit, vous souhaiterez probablement utiliser un accès direct aux attributs pour accéder à un membre de la classe. Par exemple, supposons que vous ayez un objet foode type Foo, qui a un membre num. Ensuite, vous pouvez simplement obtenir ce membre avec num = foo.num. Au fur et à mesure que votre projet se développe, vous pouvez avoir l'impression qu'il doit y avoir des vérifications ou des débogages sur l'accès aux attributs simples. Ensuite, vous pouvez le faire avec un@property intérieur la classe. L'interface d'accès aux données reste la même, de sorte qu'il n'est pas nécessaire de modifier le code client.

    Cité de PEP-8 :

    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 offre 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.

  • L'utilisation @propertypour l'accès aux données en Python est considérée comme Pythonic :

    • Il peut renforcer votre auto-identification en tant que programmeur Python (pas Java).

    • Cela peut aider votre entretien d'embauche si votre enquêteur pense que les getters et setters de style Java sont anti-modèles .

Avantages des getters et setters traditionnels

  • Les getters et setters traditionnels permettent un accès aux données plus compliqué qu'un simple accès aux attributs. Par exemple, lorsque vous définissez un membre de classe, vous avez parfois besoin d'un indicateur indiquant où vous souhaitez forcer cette opération même si quelque chose ne semble pas parfait. Bien qu'il ne soit pas évident de savoir comment augmenter un accès direct à un membre foo.num = num, vous pouvez facilement augmenter votre setter traditionnel avec un forceparamètre supplémentaire :

    def Foo:
        def set_num(self, num, force=False):
            ...
  • Les getters et setters traditionnels expliquent clairement que l'accès d'un membre de classe se fait via une méthode. Ça signifie:

    • Ce que vous obtenez comme résultat peut ne pas être identique à ce qui est exactement stocké dans cette classe.

    • Même si l'accès ressemble à un simple accès aux attributs, les performances peuvent varier considérablement.

    À moins que les utilisateurs de votre classe n'attendent un @property cacher derrière chaque déclaration d'accès aux attributs, rendre ces choses explicites peut aider à minimiser les surprises de vos utilisateurs de classe.

  • Comme mentionné par @NeilenMarais et dans cet article , l'extension des getters et setters traditionnels dans les sous-classes est plus facile que l'extension des propriétés.

  • Les getters et setters traditionnels sont largement utilisés depuis longtemps dans différentes langues. Si vous avez des gens d'horizons différents dans votre équipe, ils semblent plus familiers que @property. De plus, à mesure que votre projet se développe, si vous devez migrer de Python vers une autre langue qui n'en a pas @property, l'utilisation de getters et setters traditionnels rendrait la migration plus fluide.

Avertissements

  • Ni @propertyles getters ni les setters traditionnels ne rendent le membre de la classe privé, même si vous utilisez un double soulignement avant son nom:

    class Foo:
        def __init__(self):
            self.__num = 0
    
        @property
        def num(self):
            return self.__num
    
        @num.setter
        def num(self, num):
            self.__num = num
    
        def get_num(self):
            return self.__num
    
        def set_num(self, num):
            self.__num = num
    
    foo = Foo()
    print(foo.num)          # output: 0
    print(foo.get_num())    # output: 0
    print(foo._Foo__num)    # output: 0

2

Voici un extrait de "Python efficace: 90 façons spécifiques d'écrire mieux en Python" (livre incroyable. Je le recommande vivement).

Choses à retenir

✦ Définissez de nouvelles interfaces de classe à l'aide d'attributs publics simples et évitez de définir des méthodes setter et getter.

✦ Utilisez @property pour définir un comportement spécial lors de l'accès aux attributs sur vos objets, si nécessaire.

✦ Suivez la règle de la moindre surprise et évitez les effets secondaires étranges dans vos méthodes @property.

✦ Assurez-vous que les méthodes @property sont rapides; pour les travaux lents ou complexes, impliquant notamment des E / S ou provoquant des effets secondaires, utilisez plutôt des méthodes normales.

Une utilisation avancée mais courante de @property consiste à transformer ce qui était autrefois un simple attribut numérique en un calcul à la volée. Ceci est extrêmement utile car il vous permet de migrer toutes les utilisations existantes d'une classe pour avoir de nouveaux comportements sans nécessiter la réécriture des sites d'appel (ce qui est particulièrement important s'il y a du code d'appel que vous ne contrôlez pas). @property fournit également un point d'arrêt important pour améliorer les interfaces au fil du temps.

J'aime particulièrement @property car il vous permet de progresser progressivement vers un meilleur modèle de données au fil du temps.
@property est un outil pour vous aider à résoudre les problèmes que vous rencontrerez dans le code du monde réel. N'en abusez pas. Lorsque vous vous trouvez à étendre à plusieurs reprises les méthodes @property, il est probablement temps de refactoriser votre classe au lieu de paver davantage sur la mauvaise conception de votre code.

✦ Utilisez @property pour donner aux attributs d'instance existants de nouvelles fonctionnalités.

✦ Progressez progressivement vers de meilleurs modèles de données en utilisant @property.

✦ Pensez à refactoriser une classe et tous les sites d'appel lorsque vous vous retrouvez à utiliser trop @property.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.