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 conceviez un programme Python, quelle approche utiliseriez-vous et pourquoi?
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. Les noms de départ marqués par un ou deux caractères soulignent simplement que l'attribut donné est un détail d'implémentation qui risque de 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 méthode normale d’accès aux attributs en Pythonic.
L’avantage des propriétés est qu’elles sont syntaxiquement identiques à l’accès aux attributs. Vous pouvez donc passer d’un élément à l’autre sans modifier le code client. Vous pourriez 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 version qui n'en utilise pas pour la production, sans changer le code qui l'utilise. En même temps, vous n'avez pas besoin d'écrire des getters et des setters pour tout, au cas où vous auriez besoin de mieux contrôler l'accès plus tard.
Dans Python, vous n'utilisez pas de getters, de setters ou de propriétés uniquement pour le plaisir. Vous commencez par utiliser des attributs, puis plus tard, si nécessaire, par la suite, vous migrez vers une propriété sans avoir à modifier le code à l'aide de vos classes.
Il existe en effet beaucoup de code d’extension .py qui utilise des accesseurs, des héritiers et des classes inutiles partout, par exemple. un simple tuple conviendrait, mais c'est du code de personnes écrivant en C++ ou Java à l'aide de Python.
Ce n'est pas du code Python.
L'utilisation de propriétés vous permet de commencer par des accès d'attributs normaux, puis sauvegardez-les ensuite avec des accesseurs et des setters si nécessaire .
La réponse courte est: la propriété gagne haut la main . Toujours.
Il y a parfois un besoin d’obstacles et de passeurs, mais même dans ce cas, je les "cacherais" au monde extérieur. Il y a de nombreuses façons de le faire dans Python (getattr
, setattr
, __getattribute__
, etc ..., mais un très concis 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 introduit le sujet des getters et des setters en Python.
[TL; DR? Vous pouvez passer à la fin pour un exemple de code.]
En fait, je préfère utiliser un autre langage, ce qui est un peu compliqué à utiliser, mais Nice si vous avez un cas d'utilisation plus complexe.
Un peu de fond d'abord.
Les propriétés sont utiles dans la mesure où elles nous permettent de gérer les paramètres et d'obtenir les valeurs de manière programmatique, tout en permettant l'accès aux attributs en tant qu'attributs. Nous pouvons transformer les "entrées" en "calculs" (essentiellement) et nous pouvons transformer les "ensembles" en "événements". Supposons donc que nous ayons la classe suivante, que j'ai codée avec des getters et des 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 la méthode __init__
de l'objet. La raison en est que pour notre cas, je veux supposer que les méthodes someDefaultComputation
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ù Pour les besoins de cet exemple, "non défini" signifie "défini sur Aucun". Je veux la valeur du calcul par défaut de x
(ou de y
]).
Donc, c'est 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 souhaitons généralement que ces méthodes agissent en plus d’obtenir et de définir des valeurs (sinon, il n’est pas utile d’utiliser des propriétés). Je l'ai fait dans mon exemple de 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. C'est un modèle très commun.
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 le bruit supplémentaire qui encombre le code.
Un autre inconvénient est que nous devons toujours initialiser les valeurs x
et y
dans __init__
. (Bien sûr, nous pourrions les ajouter en utilisant setattr()
mais c'est du code supplémentaire.)
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 on prend des paramètres, ce n'est pas un getter! Dans un sens officiel, c'est vrai. Mais dans la pratique, il n’ya aucune raison pour que nous ne soyons pas en mesure de paramétrer un attribut nommé - comme x
- et de 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 puissions obtenir est de remplacer l'assignation 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 auteur 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 d'autre.
Mais même si nous le faisions, nous ne pourrions toujours pas le supporter avec des propriétés car il n’existait aucun moyen d’obtenir la valeur car nous ne pouvions pas du tout transmettre de paramètres au getter. Nous avons donc dû tout renvoyer, en introduisant une asymétrie.
Le getter/setter de style Java nous permet de gérer cela, mais nous avons de nouveau besoin de getter/setters.
Dans mon esprit, ce que nous voulons vraiment, c’est quelque chose qui capture les exigences suivantes:
Les utilisateurs définissent une seule méthode pour un attribut donné et peuvent 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. Par conséquent, nous n'avons pas besoin de __init__
ou setattr
dans le code. La variable existe simplement du fait que nous avons créé cet attribut new-style.
Tout code par défaut pour l'attribut est exécuté dans le corps même de la méthode.
Nous pouvons définir l'attribut en tant qu'attribut et le référencer en tant qu'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 souhaitons également un moyen de procéder de la sorte dans 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é cette question ci-dessous.
Maintenant au point (yay! Le point!). La solution pour laquelle j'ai proposé cette solution 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 définie, mais conserve également un handle sur le code qui sait comment calculer une valeur par défaut. Son travail consiste à stocker l'ensemble value
ou à exécuter le method
si cette valeur n'est pas définie.
Appelons cela 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 method
voici une méthode de classe, value
est la valeur de la UberProperty
, et j’ai ajouté isSet
car None
peut être une valeur réelle et Cela nous permet de déclarer clairement qu’il n’ya vraiment aucune valeur. Une autre manière est une sentinelle.
Cela nous donne fondamentalement un objet qui peut faire ce que nous voulons, mais comment pouvons-nous le mettre réellement 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 à un simple '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 nous assurer qu'il gère à la fois les acquisitions et les ensembles.
Commençons par obtient.
Ma première tentative a été de créer simplement un nouvel objet UberProperty et de le renvoyer:
def uberProperty(f):
return UberProperty(f)
Bien sûr, j'ai vite découvert que cela ne fonctionnait pas: Python ne lie jamais l'appelable à l'objet et j'ai besoin de l'objet pour appeler la fonction. Même créer le décorateur dans la classe ne fonctionne pas, car bien que maintenant nous avons la classe, nous n'avons toujours pas d'objet avec lequel travailler.
Nous allons donc avoir besoin de 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 UberProperty
pour ne stocker que la référence method
:
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 le tableau? Eh bien, que finissons-nous lorsque nous créons l'exemple de classe à l'aide de 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 la UberProperty
qui, bien entendu, n'est pas appelable, ce qui ne sert donc à rien.
Nous avons besoin d'un moyen de lier dynamiquement l'instance 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 renvoyé à cet utilisateur pour utilisation. Euh, ouais, c'est un appel __init__
, mec.
Écrivons ce que nous voulons que notre résultat de recherche soit le premier. Nous lions un UberProperty
à une instance, une chose évidente à renvoyer serait donc BoundUberProperty. C’est là que nous allons réellement maintenir l’état pour l’attribut x
.
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 obtenir sur un objet? Il existe quelques approches, mais la plus facile à expliquer utilise simplement la méthode __init__
pour effectuer ce mappage. Au moment où __init__
s'appelle nos décorateurs ont exécuté, il suffit donc de regarder à travers le __dict__
de l'objet et de mettre à jour tous les attributs pour lesquels la valeur de l'attribut est de type UberProperty
.
Maintenant, uber-properties est cool et nous voudrons probablement les utiliser beaucoup, il est donc logique de créer une classe de base qui le fait pour toutes les sous-classes. Je pense que vous savez comment on appellera la classe de base.
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 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 le résultat souhaité:
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 j'ai utilisé getValue
, setValue
et clearValue
ici. C'est parce que je n'ai pas encore de lien dans les moyens de les renvoyer automatiquement.
Mais je pense que c’est un bon endroit pour arrêter pour le moment, car je commence à être fatiguée. Vous pouvez également constater que la fonctionnalité essentielle que nous recherchions est en place. le reste est habillé de fenêtre. Important habillage de la fenêtre d’utilisation, mais cela peut attendre jusqu’à ce que j’ai un changement pour mettre à jour le message.
Je terminerai l’exemple dans le prochain message en abordant les points suivants:
Nous devons nous assurer que le __init__
d'UberObject est toujours appelé par les sous-classes.
Nous devons nous assurer de gérer le cas courant dans lequel une personne "alias" remplit une fonction, par exemple:
class Example(object):
@uberProperty
def x(self):
...
y = x
Nous avons besoin de e.x
pour renvoyer e.x.getValue()
par défaut.
e.x.getValue()
. (Faire celui-ci est évident, si vous ne l'avez pas déjà corrigé.)Nous devons prendre en charge le réglage de e.x directly
, comme dans e.x = <newvalue>
. Nous pouvons également faire cela dans la classe parente, mais nous devons mettre à jour notre code __init__
pour le gérer.
Enfin, nous ajouterons des attributs paramétrés. Il devrait être assez évident de savoir comment nous allons faire cela aussi.
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 peux être en retard sur si c'est toujours le cas.
Je pense que les deux ont leur place. Un problème lié à l'utilisation de @property
est qu'il est difficile d'étendre le comportement des getters ou des setters dans les sous-classes à l'aide de mécanismes de classe standard. Le problème est que les fonctions de lecture/définition sont masquées dans la propriété.
Vous pouvez réellement obtenir 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 en tant que C.p.fget
et C.p.fset
, mais vous ne pouvez pas utiliser facilement les fonctionnalités normales d'héritage de méthode (par exemple, super) pour les étendre. Après avoir fouillé dans les subtilités de super, vous pouvez utilisez vraiment super de cette façon:
# 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 toutefois assez fastidieuse, car la propriété doit être redéfinie et vous devez utiliser le mécanisme super légèrement contre-intuitif (cls, cls) pour obtenir une copie non liée de p.
L'utilisation de 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
vs.
o.setX(5)
ox = o.getX()
est pour moi tout à fait évident qui est plus facile à lire. De plus, les propriétés permettent de créer des variables privées beaucoup plus facilement.
Je préférerais ne pas utiliser ni 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 auprès d'un passeur. 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.
En conséquence, l’exception risque de ne pas être gérée et de se propager trop haut dans la chaîne d’appel pour être gérée correctement, ou de donner lieu à un traçage très inutile présenté à l’utilisateur du programme (ce qui est malheureusement trop commun dans le monde de python et Java).
J'éviterais aussi d'utiliser des getters et des setters:
Au lieu des propriétés et des getters/setters, je préfère effectuer 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 similaire Account.save.
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 qu'il peut être préférable de rendre vos classes simples et transparentes au point de ne pas en avoir besoin.
Je pense que les propriétés ont pour objectif de vous permettre d’obtenir le temps d’écriture nécessaire pour écrire des accesseurs et des passeurs uniquement lorsque vous en avez réellement besoin.
La culture de programmation Java recommande fortement de ne jamais donner accès aux propriétés, mais plutôt de passer par des accesseurs et des installateurs, et uniquement ceux qui sont réellement nécessaires. Il est un peu verbeux de toujours écrire ces éléments de code évidents et de noter qu'ils ne sont jamais remplacés par une logique non triviale dans 70% des cas.
En Python, les gens s’occupent de ce genre de frais généraux, de sorte que vous puissiez adopter la pratique suivante:
@property
pour les implémenter sans changer la syntaxe du reste de votre code.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.setter
qui rendent le débogage encore plus difficile:
1) python permet de créer de nouveaux attributs pour un objet existant. Cela rend très difficile le suivi d'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) Un passeur peut parfois évoluer vers une méthode complexe et lente (par exemple, frapper une base de données). Il serait très difficile pour un autre développeur de comprendre pourquoi la fonction suivante est très lente. Il pourrait passer beaucoup de temps sur le profilage de la méthode do_something()
alors que my_object.my_attr = 4.
est en réalité la cause du ralentissement:
def slow_function(my_object):
my_object.my_attr = 4.
my_object.do_something()
_@property
_ et les getters et setters traditionnels ont leurs avantages. Cela dépend de votre cas d'utilisation.
@property
_Il n'est pas nécessaire de modifier l'interface en modifiant l'implémentation de l'accès aux données. Lorsque votre projet est petit, vous souhaiterez probablement utiliser un accès d'attribut direct pour accéder à un membre de la classe. Par exemple, supposons que vous ayez un objet foo
de 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 besoin de vérifier ou de déboguer l'accès simple aux attributs. Ensuite, vous pouvez le faire avec un _@property
_ dans de 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 publics 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 moyen simple d'améliorer les améliorations futures, si vous estimez qu'un attribut de données simple doit développer son comportement fonctionnel. Dans ce cas, utilisez des propriétés pour masquer l'implémentation fonctionnelle derrière la syntaxe d'accès aux attributs de données simples.
L'utilisation de _@property
_ pour l'accès aux données dans Python est considérée comme Pythonic :
Il peut renforcer votre auto-identification en tant que programmeur Python (et non Java).
Votre entretien d'embauche peut être utile si votre interlocuteur pense que les getters et les setters de style Java sont anti-patterns .
Les getters et les 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 du groupe, vous avez parfois besoin d'un indicateur indiquant l'endroit où vous souhaitez forcer cette opération même si quelque chose ne semble pas parfait. Bien qu'il ne soit pas évident d'augmenter l'extension d'un accès direct au membre tel que _foo.num = num
_, vous pouvez facilement augmenter votre setter traditionnel avec un paramètre supplémentaire force
:
_def Foo:
def set_num(self, num, force=False):
...
_
Les getters et les setters traditionnels expliquent qu'un accès membre de la classe se fait par le biais d'une méthode. Ça signifie:
Le résultat obtenu peut ne pas être identique à ce qui est exactement stocké dans cette classe.
Même si l'accès ressemble à un simple accès attributaire, les performances peuvent varier considérablement.
À moins que les utilisateurs de la classe s'attendent à ce que _@property
_ se cache derrière chaque instruction d'accès aux attributs, rendre ces informations explicites peut aider à minimiser les surprises de vos utilisateurs.
Comme mentionné par @ NeilenMarais et dans cet article , il est plus facile d'étendre les accesseurs et les setters traditionnels dans des sous-classes que d'étendre les propriétés.
Les getters et les setters traditionnels sont largement utilisés depuis longtemps dans différentes langues. Si vous avez des personnes d'horizons différents dans votre équipe, elles ont l'air plus familières que _@property
_. De plus, à mesure que votre projet se développe, si vous deviez migrer de Python vers une autre langue qui n'a pas _@property
_, l'utilisation des accesseurs et des régulateurs traditionnels faciliterait la migration.
Ni _@property
_ ni les getters et les setters traditionnels ne rendent le membre du groupe privé, même si vous utilisez un double soulignement (soulignement double) 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
_