[ 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é defaultX
et defaultY
dans la __init__
méthode de l'objet . La raison en est que dans notre cas, je veux supposer que les someDefaultComputation
mé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 y
de).
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 x
et . (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 setattr
dans 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 value
ou à 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 que method
c'est une méthode de classe, value
c'est la valeur de la UberProperty
, et j'ai ajouté isSet
parce que None
peut ê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 uberProperty
et 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 UberProperty
pour ne stocker que la method
ré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 UberProperty
qui 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' UberProperty
instance 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' 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, 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é 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 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
, setValue
et clearValue
ici. 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.x
revenir 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.