[ TL;DR? Sie können Springen Sie zum Ende, um ein Codebeispiel zu sehen .]
Ich ziehe es vor, ein anderes Idiom zu verwenden, das ein wenig kompliziert ist, um es einmalig zu verwenden, aber es ist gut, wenn man einen komplexeren Anwendungsfall hat.
Zunächst ein paar Hintergrundinformationen.
Eigenschaften sind insofern nützlich, als sie es uns ermöglichen, sowohl das Setzen als auch das Abrufen von Werten auf programmatische Weise zu handhaben, aber dennoch den Zugriff auf Attribute als Attribute zu ermöglichen. Wir können "gets" in "computations" verwandeln (im Wesentlichen) und wir können "sets" in "events" verwandeln. Nehmen wir also an, wir haben die folgende Klasse, die ich mit Java-ähnlichen Gettern und Settern kodiert habe.
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()
Sie fragen sich vielleicht, warum ich nicht angerufen habe defaultX
y defaultY
im Objekt __init__
Methode. Der Grund dafür ist, dass ich für unseren Fall annehmen möchte, dass die someDefaultComputation
Methoden geben Werte zurück, die sich im Laufe der Zeit ändern, z. B. einen Zeitstempel, und wenn x
(oder y
) nicht gesetzt ist (wobei "nicht gesetzt" in diesem Beispiel "auf Null gesetzt" bedeutet), möchte ich den Wert von x
's (oder y
) Standardberechnung.
Das ist also aus einer Reihe von Gründen, die oben beschrieben wurden, lahm. Ich werde es mit Eigenschaften neu schreiben:
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.
Was haben wir gewonnen? Wir haben die Möglichkeit gewonnen, diese Attribute als Attribute zu bezeichnen, auch wenn wir hinter den Kulissen Methoden ausführen.
Die eigentliche Stärke von Eigenschaften liegt natürlich darin, dass wir in der Regel wollen, dass diese Methoden etwas anderes tun, als nur Werte abzurufen und zu setzen (andernfalls wäre es sinnlos, Eigenschaften zu verwenden). Ich habe dies in meinem Getter-Beispiel getan. Wir führen im Grunde einen Funktionskörper aus, um einen Standardwert zu erhalten, wenn der Wert nicht gesetzt ist. Dies ist ein sehr gängiges Muster.
Aber was verlieren wir, und was können wir nicht tun?
Das größte Ärgernis ist meiner Meinung nach, dass man, wenn man einen Getter definiert (wie wir es hier tun), auch einen Setter definieren muss[1] - ein zusätzliches Ärgernis, das den Code unübersichtlich macht.
Ein weiteres Ärgernis ist, dass wir immer noch die x
y y
Werte in __init__
. (Natürlich können wir sie auch mit setattr()
aber das ist mehr zusätzlicher Code).
Drittens können Getter im Gegensatz zu dem Java-ähnlichen Beispiel keine anderen Parameter akzeptieren. Ich höre Sie jetzt schon sagen, na ja, wenn es Parameter annimmt, ist es kein Getter! Im offiziellen Sinne ist das richtig. Aber in einem praktischen Sinn gibt es keinen Grund, warum wir nicht in der Lage sein sollten, ein benanntes Attribut zu parametrisieren -- wie x
-- und setzen seinen Wert für einige spezifische Parameter.
Es wäre schön, wenn wir so etwas tun könnten:
e.x[a,b,c] = 10
e.x[d,e,f] = 20
zum Beispiel. Das Naheliegendste, was wir tun können, ist, die Zuweisung zu überschreiben, um eine spezielle Semantik zu implizieren:
e.x = [a,b,c,10]
e.x = [d,e,f,30]
und natürlich sicherstellen, dass unser Setter weiß, wie man die ersten drei Werte als Schlüssel für ein Wörterbuch extrahiert und seinen Wert auf eine Zahl oder etwas anderes setzt.
Aber selbst wenn wir das täten, könnten wir es immer noch nicht mit Eigenschaften unterstützen, weil es keine Möglichkeit gibt, den Wert zu erhalten, weil wir dem Getter überhaupt keine Parameter übergeben können. Wir mussten also alles zurückgeben, was zu einer Asymmetrie führte.
Die Java-Stil Getter/Setter lässt uns dies zu behandeln, aber wir sind zurück zu benötigen Getter/Setter.
Meiner Meinung nach brauchen wir etwas, das die folgenden Anforderungen erfüllt:
-
Die Benutzer definieren nur eine Methode für ein bestimmtes Attribut und können dort angeben ob das Attribut schreibgeschützt oder schreibgeschützt ist. Eigenschaften bestehen diesen Test nicht wenn das Attribut beschreibbar ist.
-
Es besteht keine Notwendigkeit für den Benutzer, eine zusätzliche Variable zu definieren, die der Funktion zugrundeliegt, so dass wir die __init__
o setattr
im Code. Die Variable existiert nur durch die Tatsache, dass wir dieses neue Attribut erstellt haben.
-
Jeder Standardcode für das Attribut wird im Methodenrumpf selbst ausgeführt.
-
Wir können das Attribut als Attribut festlegen und es als Attribut referenzieren.
-
Wir können das Attribut parametrisieren.
Was den Code betrifft, wollen wir eine Möglichkeit zum Schreiben:
def x(self, *args):
return defaultX()
und in der Lage sein, dies zu tun:
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
und so weiter.
Wir wollen auch eine Möglichkeit, dies für den speziellen Fall eines parametrisierbaren Attributs zu tun, aber dennoch den Standard-Zuweisungsfall zu ermöglichen. Sie werden sehen, wie ich das unten angegangen bin.
Nun zur Sache (juhu! zur Sache!). Die Lösung, die ich mir dafür ausgedacht habe, lautet wie folgt.
Wir erstellen ein neues Objekt, das den Begriff der Eigenschaft ersetzt. Das Objekt ist dazu gedacht, den Wert einer Variablen zu speichern, die ihm zugewiesen wurde, aber auch eine Handhabe für Code zu haben, der weiß, wie man einen Standardwert berechnet. Seine Aufgabe ist es, die Menge zu speichern value
oder um die method
wenn dieser Wert nicht gesetzt ist.
Nennen wir es eine 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
Ich nehme an method
hier ist eine Klassenmethode, value
ist der Wert der UberProperty
und ich habe hinzugefügt isSet
porque None
kann ein echter Wert sein, und dies ermöglicht uns eine saubere Art zu erklären, dass es wirklich "keinen Wert" gibt. Eine andere Möglichkeit ist eine Art Sentinel.
Damit haben wir im Grunde ein Objekt, das tun kann, was wir wollen, aber wie können wir es in unsere Klasse einbauen? Nun, Eigenschaften verwenden Dekoratoren; warum können wir das nicht? Schauen wir uns an, wie es aussehen könnte (von hier an werde ich nur noch ein einziges 'Attribut' verwenden, x
).
class Example(object):
@uberProperty
def x(self):
return defaultX()
Das funktioniert natürlich noch nicht wirklich. Wir müssen implementieren uberProperty
und stellen Sie sicher, dass es sowohl gets als auch sets verarbeitet.
Beginnen wir mit "gets".
Mein erster Versuch war, einfach ein neues UberProperty-Objekt zu erstellen und es zurückzugeben:
def uberProperty(f):
return UberProperty(f)
Ich habe natürlich schnell festgestellt, dass das nicht funktioniert: Python bindet den Callable nie an das Objekt und ich brauche das Objekt, um die Funktion aufzurufen. Auch das Erstellen des Dekorators in der Klasse funktioniert nicht, denn obwohl wir jetzt die Klasse haben, haben wir immer noch kein Objekt, mit dem wir arbeiten können.
Wir müssen hier also mehr tun können. Wir wissen, dass eine Methode nur ein einziges Mal dargestellt werden muss, also behalten wir unseren Dekorator bei, ändern aber UberProperty
um nur die method
Hinweis:
class UberProperty(object):
def __init__(self, method):
self.method = method
Sie ist auch nicht aufrufbar, so dass im Moment nichts funktioniert.
Wie können wir das Bild vervollständigen? Nun, was haben wir am Ende, wenn wir die Beispielklasse mit unserem neuen Dekorator erstellen:
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>
in beiden Fällen erhalten wir die UberProperty
die natürlich nicht aufrufbar ist, so dass dies nicht von großem Nutzen ist.
Was wir brauchen, ist ein Weg, um dynamisch die UberProperty
Instanz, die vom Dekorator nach der Erstellung der Klasse erzeugt wurde, in ein Objekt der Klasse umzuwandeln, bevor dieses Objekt an den Benutzer zur Verwendung zurückgegeben wird. Ähm, ja, das ist ein __init__
anrufen, Kumpel.
Lassen Sie uns zuerst aufschreiben, was unser Suchergebnis sein soll. Wir binden eine UberProperty
zu einer Instanz, so dass es naheliegend wäre, eine BoundUberProperty zurückzugeben. Hier werden wir tatsächlich den Zustand für die 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
Nun zur Darstellung: Wie bekommt man diese auf ein Objekt? Es gibt einige Ansätze, aber der am einfachsten zu erklärende verwendet einfach die __init__
Methode, um diese Zuordnung vorzunehmen. Zu der Zeit __init__
aufgerufen wird, sind unsere Dekorateure gelaufen, wir müssen also nur die __dict__
und aktualisieren Sie alle Attribute, bei denen der Wert des Attributs vom Typ UberProperty
.
Nun, Uber-Properties sind cool und wir werden sie wahrscheinlich oft verwenden wollen, daher ist es sinnvoll, eine Basisklasse zu erstellen, die dies für alle Unterklassen tut. Ich denke, Sie wissen, wie die Basisklasse genannt werden soll.
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)
Wir fügen dies hinzu, ändern unser Beispiel und erben von UberObject
und ...
e = Example()
print e.x -> <__main__.BoundUberProperty object at 0x104604c90>
Nach der Änderung von x
zu sein:
@uberProperty
def x(self):
return *datetime.datetime.now()*
Wir können einen einfachen Test durchführen:
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()
Und wir erhalten das gewünschte Ergebnis:
2013-05-31 00:05:13.985813
2013-05-31 00:05:13.986290
2013-05-31
2013-05-31 00:05:13.986310
(Oh je, ich arbeite lange.)
Beachten Sie, dass ich getValue
, setValue
y clearValue
hier. Das liegt daran, dass ich noch keine Möglichkeit gefunden habe, diese automatisch zurückzusenden.
Aber ich denke, das ist ein guter Zeitpunkt, um aufzuhören, denn ich werde müde. Sie können auch sehen, dass die Kernfunktionalität, die wir wollten, vorhanden ist; der Rest ist Augenwischerei. Der Rest ist Schaufensterdekoration, aber das kann warten, bis ich die Gelegenheit habe, den Beitrag zu aktualisieren.
Ich werde das Beispiel im nächsten Beitrag abschließen, indem ich auf diese Dinge eingehe:
-
Wir müssen sicherstellen, dass die UberObjects __init__
wird immer von Unterklassen aufgerufen.
- Also zwingen wir entweder, dass sie irgendwo aufgerufen wird, oder wir verhindern, dass sie implementiert wird.
- Wir werden sehen, wie man das mit einer Metaklasse macht.
-
Wir müssen sicherstellen, dass wir den häufigen Fall behandeln, dass jemand ein "Alias" für eine Funktion auf etwas anderes "aliasiert", wie zum Beispiel:
class Example(object):
@uberProperty
def x(self):
...
y = x
-
Wir brauchen e.x
zur Rückkehr e.x.getValue()
standardmäßig.
- Wir werden sehen, dass dies ein Bereich ist, in dem das Modell versagt.
- Es stellt sich heraus, dass wir immer einen Funktionsaufruf verwenden müssen, um den Wert zu erhalten.
- Aber wir können es wie einen normalen Funktionsaufruf aussehen lassen und vermeiden, dass wir die
e.x.getValue()
. (Dies zu tun, ist offensichtlich, falls Sie es nicht schon herausgefunden haben).
-
Wir müssen die Einstellung unterstützen e.x directly
wie in e.x = <newvalue>
. Wir können dies auch in der übergeordneten Klasse tun, aber wir müssen unsere __init__
Code, um damit umzugehen.
-
Schließlich werden wir parametrisierte Attribute hinzufügen. Es sollte ziemlich offensichtlich sein, wie wir das machen.
Hier ist der Code, wie er bis jetzt existiert:
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] Ob dies immer noch der Fall ist, kann ich nicht beurteilen.