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