[ TL; DR? Sie können bis zum Ende springen, um ein Codebeispiel zu erhalten .]
Eigentlich bevorzuge ich die Verwendung einer anderen Redewendung, die für die einmalige Verwendung etwas aufwendig ist, aber schön ist, wenn Sie einen komplexeren Anwendungsfall haben.
Zuerst ein bisschen Hintergrund.
Eigenschaften sind insofern nützlich, als sie es uns ermöglichen, sowohl das Einstellen als auch das Abrufen von Werten programmgesteuert zu behandeln, aber dennoch den Zugriff auf Attribute als Attribute zu ermöglichen. Wir können "Gets" (im Wesentlichen) in "Berechnungen" und "Sets" in "Ereignisse" verwandeln. Nehmen wir also an, wir haben die folgende Klasse, die ich mit Java-ähnlichen Gettern und Setzern codiert 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 aufgerufen habe defaultX
und defaultY
in der __init__
Methode des Objekts . Der Grund ist, dass ich für unseren Fall annehmen möchte, dass die someDefaultComputation
Methoden Werte zurückgeben, die sich über die Zeit ändern, beispielsweise einen Zeitstempel, und wann immer x
(oder y
) nicht gesetzt ist (wobei für den Zweck dieses Beispiels "nicht gesetzt" "gesetzt" bedeutet to None ") Ich möchte den Wert der Standardberechnung von x
's (oder y
' s).
Dies ist aus einer Reihe von Gründen, die oben beschrieben wurden, lahm. Ich werde es mit Eigenschaften umschreiben:
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 erhalten, diese Attribute als Attribute zu bezeichnen, obwohl wir hinter den Kulissen Methoden ausführen.
Die wahre Stärke von Eigenschaften besteht natürlich darin, dass diese Methoden im Allgemeinen nicht nur Werte abrufen und festlegen sollen (andernfalls macht es keinen Sinn, Eigenschaften zu verwenden). Ich habe das in meinem Getter-Beispiel gemacht. Grundsätzlich führen wir einen Funktionskörper aus, um einen Standardwert zu ermitteln, wenn der Wert nicht festgelegt ist. Dies ist ein sehr verbreitetes Muster.
Aber was verlieren wir und was können wir nicht tun?
Meiner Ansicht nach ist der größte Ärger, dass Sie, wenn Sie einen Getter definieren (wie wir es hier tun), auch einen Setter definieren müssen. [1] Das ist zusätzliches Rauschen, das den Code überfüllt.
Ein weiterer Ärger ist, dass wir die x
und y
-Werte noch initialisieren müssen __init__
. (Nun, natürlich könnten wir sie mit hinzufügen, setattr()
aber das ist mehr zusätzlicher Code.)
Drittens können Getter im Gegensatz zum Java-ähnlichen Beispiel keine anderen Parameter akzeptieren. Jetzt kann ich Sie schon sagen hören, na ja, wenn es Parameter nimmt, ist es kein Getter! Im offiziellen Sinne ist das wahr. In praktischer Hinsicht gibt es jedoch keinen Grund, warum wir nicht in der Lage sein sollten, ein benanntes Attribut wie zu parametrisieren x
und seinen Wert für bestimmte Parameter festzulegen.
Es wäre schön, wenn wir so etwas machen könnten:
e.x[a,b,c] = 10
e.x[d,e,f] = 20
zum Beispiel. Das Beste, was wir bekommen 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 setzt.
Aber selbst wenn wir das tun würden, könnten wir es nicht mit Eigenschaften unterstützen, da es keine Möglichkeit gibt, den Wert abzurufen, da wir überhaupt keine Parameter an den Getter übergeben können. Wir mussten also alles zurückgeben und eine Asymmetrie einführen.
Der Java-artige Getter / Setter lässt uns damit umgehen, aber wir brauchen wieder Getter / Setter.
In meinen Augen wollen wir wirklich etwas, das die folgenden Anforderungen erfüllt:
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 schlagen diesen Test fehl, wenn das Attribut beschreibbar ist.
Der Benutzer muss keine zusätzliche Variable definieren, die der Funktion zugrunde liegt, daher benötigen wir das __init__
oder setattr
im Code nicht. Die Variable existiert nur dadurch, dass wir dieses Attribut im neuen Stil erstellt haben.
Jeder Standardcode für das Attribut wird im Methodenkörper selbst ausgeführt.
Wir können das Attribut als Attribut festlegen und als Attribut referenzieren.
Wir können das Attribut parametrisieren.
In Bezug auf Code wollen wir eine Möglichkeit zu schreiben:
def x(self, *args):
return defaultX()
und dann in der Lage sein:
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 möchten dies auch für den Sonderfall eines parametrierbaren Attributs tun, aber dennoch zulassen, dass der Standard-Zuweisungsfall funktioniert. Sie werden unten sehen, wie ich das angegangen bin.
Nun zum Punkt (yay! Der Punkt!). Die Lösung, die ich dafür gefunden habe, ist wie folgt.
Wir erstellen ein neues Objekt, um den Begriff einer Eigenschaft zu ersetzen. Das Objekt soll den Wert einer darauf gesetzten Variablen speichern, verwaltet jedoch auch ein Handle für Code, der weiß, wie ein Standard berechnet wird. Seine Aufgabe ist es, die Menge zu speichern value
oder die auszuführen, method
wenn dieser Wert nicht gesetzt ist.
Nennen wir es ein 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 von UberProperty
, und ich habe hinzugefügt, isSet
weil es sich None
möglicherweise um einen echten Wert handelt, und dies ermöglicht uns eine saubere Möglichkeit, zu erklären, dass es wirklich "keinen Wert" gibt. Ein anderer Weg ist eine Art Wachposten.
Dies gibt uns im Grunde ein Objekt, das tun kann, was wir wollen, aber wie setzen wir es tatsächlich in unsere Klasse ein? Nun, Eigenschaften verwenden Dekorateure; warum können wir nicht Mal sehen, wie es aussehen könnte (von jetzt 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. Wir müssen implementieren uberProperty
und sicherstellen, dass es sowohl Gets als auch Sets verarbeitet.
Beginnen wir mit bekommen.
Mein erster Versuch war, einfach ein neues UberProperty-Objekt zu erstellen und es zurückzugeben:
def uberProperty(f):
return UberProperty(f)
Ich stellte natürlich schnell fest, dass dies nicht funktioniert: Python bindet den Aufrufbaren niemals an das Objekt und ich benötige das Objekt, um die Funktion aufzurufen. Selbst das Erstellen des Dekorators in der Klasse funktioniert nicht, da wir jetzt zwar die Klasse haben, aber immer noch kein Objekt haben, mit dem wir arbeiten können.
Wir müssen hier also mehr tun können. Wir wissen, dass eine Methode nur einmal dargestellt werden muss. Lassen Sie uns also unseren Dekorateur behalten, aber ändern Sie sie, UberProperty
um nur die method
Referenz zu speichern :
class UberProperty(object):
def __init__(self, method):
self.method = method
Es ist auch nicht aufrufbar, so dass im Moment nichts funktioniert.
Wie vervollständigen wir das Bild? 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 das zurück, UberProperty
was natürlich nicht aufrufbar ist, daher ist dies nicht sehr nützlich.
Was wir brauchen, ist eine Möglichkeit, UberProperty
die vom Dekorateur erstellte Instanz dynamisch zu binden, nachdem die Klasse an ein Objekt der Klasse erstellt wurde, bevor dieses Objekt zur Verwendung an diesen Benutzer zurückgegeben wurde. Ähm, ja, das ist ein __init__
Anruf, Alter.
Schreiben wir auf, was unser Suchergebnis zuerst sein soll. Wir binden eine UberProperty
an eine Instanz, daher ist es naheliegend, eine BoundUberProperty zurückzugeben. Hier behalten wir den Status für das x
Attribut bei.
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
Jetzt haben wir die Darstellung; Wie bringen Sie diese auf ein Objekt? Es gibt einige Ansätze, aber der am einfachsten zu erklärende verwendet nur die __init__
Methode, um dieses Mapping durchzuführen. Zu dem Zeitpunkt __init__
, an dem unsere Dekorateure aufgerufen sind, müssen sie nur noch die Objekte durchsehen __dict__
und alle Attribute aktualisieren, bei denen der Wert des Attributs vom Typ ist UberProperty
.
Jetzt sind Uber-Eigenschaften cool und wir werden sie wahrscheinlich häufig verwenden wollen. Daher ist es sinnvoll, nur eine Basisklasse zu erstellen, die dies für alle Unterklassen tut. Ich denke, Sie wissen, wie die Basisklasse heißen wird.
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, um von zu erben UberObject
, und ...
e = Example()
print e.x -> <__main__.BoundUberProperty object at 0x104604c90>
Nach dem Ändern 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 bekommen die Ausgabe, die wir wollten:
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, ich arbeite spät.)
Beachten Sie, dass ich verwendet habe getValue
, setValue
und clearValue
hier. Dies liegt daran, dass ich noch nicht die Mittel verknüpft habe, um diese automatisch zurückzugeben.
Aber ich denke, dies ist ein guter Ort, um vorerst anzuhalten, weil ich müde werde. Sie können auch sehen, dass die von uns gewünschte Kernfunktionalität vorhanden ist. Der Rest ist Schaufensterdekoration. Wichtige Usability-Fensterdekoration, aber das kann warten, bis ich eine Änderung habe, um den Beitrag zu aktualisieren.
Ich werde das Beispiel im nächsten Beitrag beenden, indem ich folgende Dinge anspreche:
Wir müssen sicherstellen, dass UberObjects __init__
immer von Unterklassen aufgerufen wird.
- Also erzwingen wir entweder, dass es irgendwo aufgerufen wird, oder wir verhindern, dass es implementiert wird.
- Wir werden sehen, wie das mit einer Metaklasse geht.
Wir müssen sicherstellen, dass wir den allgemeinen Fall behandeln, in dem jemand eine Funktion auf etwas anderes 'aliasisiert', wie zum Beispiel:
class Example(object):
@uberProperty
def x(self):
...
y = x
Wir müssen e.x
zurück in e.x.getValue()
der Standardeinstellung.
- Was wir tatsächlich sehen werden, ist, 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, es verwenden zu müssen
e.x.getValue()
. (Dies zu tun ist offensichtlich, wenn Sie es noch nicht behoben 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 unseren __init__
Code aktualisieren , um damit umzugehen.
Schließlich werden wir parametrisierte Attribute hinzufügen. Es sollte ziemlich offensichtlich sein, wie wir das auch machen werden.
Hier ist der Code, wie er bisher 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] Ich bin möglicherweise im Rückstand, ob dies noch der Fall ist.