797 votes

Utilisation de @property contre getters et setters

Voici une question de conception purement 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 d'une manière ou d'une autre. Si vous deviez concevoir un programme Python, quelle approche utiliseriez-vous et pourquoi ?

662voto

kindall Points 60645

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

La raison en est que tous les attributs sont publics en Python. Commencer les noms par un ou deux traits de soulignement est juste un avertissement que l'attribut donné est un détail d'implémentation qui peut 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 standard aux attributs est la façon normale et pythique d'accéder aux attributs.

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

108 votes

Les noms d'attributs comportant un double soulignement sont traités de manière spéciale par Python ; il ne s'agit pas d'une simple convention. Voir docs.python.org/py3k/tutorial/classes.html#private-variables

71 votes

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

4 votes

Et parce que les caractères "@" sont moches dans le code python, et que le déréférencement des @decorators donne la même impression de code spaghetti.

176voto

6502 Points 42700

En Python, vous n'utilisez pas de getters, setters ou propriétés juste pour le plaisir. Vous utilisez d'abord simplement des attributs, puis plus tard, seulement si nécessaire, vous migrez éventuellement vers une propriété sans avoir à modifier le code utilisant 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.

0 votes

Les getters et setters sont plus importants en Java qu'en C++. Probablement parce que le C++ a des références, et que si vous avez vraiment besoin de propriétés, vous pouvez les simuler avec des modèles.

55 votes

@6502, quand vous dites "[ ] classes inutiles partout où par exemple un simple tuple ferait l'affaire" : l'avantage d'une classe par rapport à un tuple, 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 faciles à lire et permettent d'éviter les erreurs que les tuples, surtout lorsqu'ils doivent être transmis en dehors du module actuel.

18 votes

@Hibou57 : Je ne dis pas que les classes 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 pas d'autre choix que de créer des classes pour tout parce que les autres possibilités sont juste ennuyeuses à utiliser dans ces langues. Un autre symptôme typique de la programmation Java/C++ en utilisant Python est la création de classes abstraites et de hiérarchies de classes complexes sans raison, alors qu'en Python, vous pourriez simplement utiliser des classes indépendantes grâce au typage en canard.

125voto

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

4 votes

@GregKrsak Ça semble étrange parce que ça l'est. Le "truc des adultes consentants" était un mème de Python avant l'ajout des propriétés. C'était la réponse habituelle aux personnes qui se plaignaient du manque de modificateurs d'accès. Lorsque les propriétés ont été ajoutées, l'encapsulation est soudainement devenue souhaitable. La même chose s'est produite avec les classes de base abstraites. "Python a toujours été en guerre contre la rupture d'encapsulation. La liberté est de l'esclavage. Les lambdas ne devraient tenir que sur une ligne."

75voto

mac Points 16282

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

Il est parfois nécessaire d'utiliser des getters et setters, mais même dans ce cas, je les "cacherais" au monde extérieur. Il existe de nombreuses façons de le faire en Python ( getattr , setattr , __getattribute__ etc..., mais il en existe un très concis et propre :

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 introduit le sujet des getters et setters en Python.

0 votes

Merci, Mac, mais ce n'est pas le but de la question (désolé si ce n'est pas clair). Il ne s'agit pas du décorateur @property, mais des propriétés par rapport aux getters/setters en général.

1 votes

@BasicWolf - Je pensais qu'il était implicitement clair que je suis du côté de la propriété de la clôture ! :) Mais j'ajoute un paragraphe à ma réponse pour le préciser.

23 votes

Indice : le mot "toujours" est un indice que l'auteur tente de vous convaincre avec une affirmation, et non un argument. Il en va de même pour la présence de caractères gras. (Je veux dire, si vous voyez des MAJUSCULES à la place, alors -- whoa -- ça doit être juste.) Regardez, la fonction "propriété" se trouve être différente de Java (le némésis de facto de Python pour une raison quelconque), et donc la pensée de groupe de la communauté Python la déclare meilleure. En réalité, les propriétés violent la règle "L'explicite est meilleur que l'implicite", mais personne ne veut l'admettre. Cela a été intégré dans le langage, alors maintenant il est déclaré "Python" via un argument tautologique.

70voto

Adam Donahue Points 351

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

En fait, je préfère utiliser un idiome différent, qui est un peu compliqué pour une utilisation ponctuelle, mais qui est intéressant si vous avez un cas d'utilisation plus complexe.

Un peu de contexte d'abord.

Les propriétés sont utiles dans la mesure où elles nous permettent de gérer la définition et l'obtention de valeurs de manière programmatique, tout en permettant d'accéder aux attributs en tant qu'attributs. Nous pouvons transformer les "obtentions" en "calculs" (essentiellement) et nous pouvons transformer les "ensembles" en "événements". Disons 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é defaultX et defaultY dans l'objet __init__ méthode. La raison en est que, dans notre cas, je veux supposer que les someDefaultComputation renvoient des valeurs qui varient dans le temps, par exemple un horodatage, et chaque fois qu'une x (ou y ) n'est pas défini (où, pour les besoins de cet exemple, "non défini" signifie "défini sur Aucun"), je veux que la valeur de l'élément x (ou y ) par défaut.

C'est donc nul pour un certain nombre de raisons décrites ci-dessus. Je vais le réécrire en utilisant des 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 gagné la capacité de nous référer à ces attributs en tant qu'attributs même si, en coulisse, nous finissons par exécuter des méthodes.

Bien sûr, la véritable puissance 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 il n'y a aucun intérêt à utiliser des propriétés). C'est ce que j'ai fait dans mon exemple de getter. En fait, nous exécutons un corps de fonction pour récupérer une valeur par défaut lorsque la valeur n'est pas définie. C'est un modèle 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], ce qui constitue un bruit supplémentaire qui encombre le code.

Un autre inconvénient est qu'il faut encore initialiser la fonction x et y valeurs en __init__ . (Bien sûr, nous pourrions les ajouter en utilisant setattr() mais c'est encore du code supplémentaire).

Troisièmement, contrairement à l'exemple de Java, les getters ne peuvent pas accepter d'autres paramètres. Je vous entends déjà dire, eh bien, si ça prend des paramètres, ce n'est pas un getter ! Dans un sens officiel, c'est vrai. Mais d'un point de vue pratique, il n'y a aucune raison pour que nous ne puissions pas 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 mieux que l'on puisse faire est de modifier 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, s'assurer 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 autre.

Mais même si nous faisions cela, nous ne pourrions toujours pas le supporter avec les propriétés car il n'y a aucun moyen d'obtenir la valeur parce que nous ne pouvons pas passer de paramètres au getter. Nous devons donc tout renvoyer, ce qui introduit une asymétrie.

Le getter/setter de style Java nous permet de gérer cela, mais nous avons à nouveau besoin de getter/setters.

À mon avis, ce que nous voulons vraiment, c'est quelque chose qui répond aux 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, donc nous n'avons pas besoin de l'attribut __init__ ou setattr dans le code. La variable existe juste par le 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 un attribut et le référencer comme un attribut.

  • Nous pouvons paramétrer l'attribut.

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

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

et être capable de le faire ensuite :

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 particulier d'un attribut paramétrable, tout en permettant au cas d'affectation par défaut de fonctionner. Vous verrez comment j'ai abordé ce problème ci-dessous.

Venons-en maintenant à l'essentiel (yay ! l'essentiel !). 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 attribuée, mais il maintient également une poignée sur le code qui sait comment calculer une valeur par défaut. Son rôle est de stocker l'ensemble value ou pour exécuter le method si 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 method voici une méthode de classe, value est la valeur de la UberProperty et j'ai ajouté isSet parce que None peut être une valeur réelle et cela nous permet de déclarer proprement qu'il n'y a vraiment "aucune valeur". Une autre solution consiste à utiliser une sorte de sentinelle.

Cela nous donne un objet qui peut faire ce que nous voulons, mais comment le mettre dans notre classe ? Eh bien, les propriétés utilisent des décorateurs ; pourquoi pas nous ? Voyons à quoi cela peut 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()

Cela ne fonctionne pas encore, bien sûr. Nous devons implémenter uberProperty et assurez-vous qu'il gère à la fois les get et les sets.

Commençons par l'obtention.

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

def uberProperty(f):
    return UberProperty(f)

J'ai rapidement découvert, bien sûr, que cela ne fonctionne pas : Python ne lie jamais le callable à 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 être en mesure de faire plus ici. Nous savons qu'une méthode ne doit être représentée qu'une seule fois, alors conservons notre décorateur, mais modifions les éléments suivants UberProperty pour ne stocker que le method référence :

class UberProperty(object):

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

Il n'est pas non plus appelable, donc pour l'instant rien ne fonctionne.

Comment compléter le tableau ? Eh bien, qu'obtenons-nous lorsque nous créons la classe d'exemple 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 le UberProperty qui, bien sûr, n'est pas une appelable, ce qui n'est pas très utile.

Ce dont nous avons besoin, c'est d'un moyen de lier dynamiquement l'option UberProperty 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 retourné à l'utilisateur pour être utilisé. Hum, oui, c'est un __init__ appelle, mec.

Commençons par écrire ce que nous voulons que le résultat de notre recherche soit. Nous lions un UberProperty à une instance, donc une chose évidente à retourner serait un BoundUberProperty. C'est ici que nous allons maintenir l'état de l'objet x attribut.

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, il s'agit de la représentation : comment les appliquer sur un objet ? Il y a quelques approches, mais la plus simple à expliquer utilise simplement la fonction __init__ pour effectuer ce mappage. Au moment où __init__ est appelé, nos décorateurs ont été exécutés, il suffit donc de regarder dans le fichier __dict__ et mettre à jour tous les attributs dont la valeur est de type UberProperty .

Maintenant, les uber-properties sont cool et nous voudrons probablement les utiliser souvent, donc il est logique de créer une classe de base qui fait cela pour toutes les sous-classes. Je pense que vous savez comment la classe de base va s'appeler.

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é x pour être :

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

Nous pouvons effectuer 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 le résultat 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

(Mince, je travaille tard.)

Notez que j'ai utilisé getValue , setValue et clearValue ici. C'est parce que je n'ai pas encore mis en place les moyens de les renvoyer automatiquement.

Mais je pense que c'est un bon endroit pour arrêter pour le moment, parce que je commence à être fatigué. Vous pouvez également voir que la fonctionnalité de base que nous voulions est en place ; le reste n'est que de la poudre aux yeux. Le reste n'est que de la poudre aux yeux. Une poudre aux yeux importante pour l'ergonomie, mais qui peut attendre que j'aie le temps de mettre à jour le billet.

Je terminerai l'exemple dans le prochain message en abordant ces points :

  • Nous devons nous assurer que l'UberObject's __init__ est toujours appelé par les sous-classes.

    • Donc, soit nous l'obligeons à être appelé quelque part, soit nous l'empêchons d'être mis en œuvre.
    • Nous allons voir comment faire cela avec une métaclasse.
  • Nous devons nous assurer que nous gérons le cas courant où quelqu'un "alias" une fonction à quelque chose d'autre. une fonction à quelque chose d'autre, par exemple :

      class Example(object):
          @uberProperty
          def x(self):
              ...
    
          y = x
  • Nous avons besoin e.x pour revenir e.x.getValue() par défaut.

    • Ce que nous allons voir, c'est que c'est un domaine où le modèle échoue.
    • Il s'avère que nous devrons toujours utiliser un appel de fonction pour obtenir la valeur.
    • Mais nous pouvons faire en sorte que cela ressemble à un appel de fonction ordinaire et éviter d'avoir à utiliser e.x.getValue() . (Faire celui-ci est évident, si vous ne l'avez pas déjà fixé).
  • Nous devons soutenir la mise en place e.x directly , comme dans e.x = <newvalue> . Nous pouvons également le faire dans la classe parent, mais nous devrons mettre à jour notre fichier __init__ pour le gérer.

  • Enfin, nous allons ajouter des attributs paramétrés. La façon dont nous allons procéder 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 ne sais pas si c'est toujours le cas.

58 votes

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

9 votes

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

0 votes

Pour info, vous peut pour que vous puissiez paramétrer le getter, en quelque sorte. Cela impliquerait de faire de la variable une classe personnalisée, dont vous auriez surchargé l'attribut __getitem__ méthode. Ce serait cependant bizarre, car vous auriez alors un python complètement non standard.

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