[ TL; DR? Puoi saltare fino alla fine per un esempio di codice .]
In realtà preferisco usare un linguaggio diverso, che è un po 'complicato da usare come una tantum, ma è bello se hai un caso d'uso più complesso.
Prima un po 'di background.
Le proprietà sono utili in quanto ci consentono di gestire sia l'impostazione che l'acquisizione di valori in modo programmatico, ma consentono comunque di accedere agli attributi come attributi. Possiamo trasformare "get" in "calcoli" (essenzialmente) e possiamo trasformare "sets" in "eventi". Quindi diciamo che abbiamo la seguente classe, che ho codificato con getter e setter simili a 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()
Forse ti starai chiedendo perché non ho chiamato defaultX
e defaultY
nel __init__
metodo dell'oggetto . Il motivo è che nel nostro caso voglio presumere che i someDefaultComputation
metodi restituiscano valori che variano nel tempo, diciamo un timestamp e ogni volta che x
(o y
) non è impostato (dove, ai fini di questo esempio, "non impostato" significa "impostato a Nessuno ") Voglio il valore del calcolo predefinito di x
's (o y
' s).
Quindi questo è zoppo per una serie di ragioni descritte sopra. Lo riscriverò usando le proprietà:
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.
Cosa abbiamo guadagnato? Abbiamo acquisito la capacità di fare riferimento a questi attributi come attributi anche se, dietro le quinte, finiamo per eseguire i metodi.
Naturalmente il vero potere delle proprietà è che generalmente desideriamo che questi metodi facciano qualcosa in aggiunta al semplice ottenere e impostare valori (altrimenti non ha senso usare le proprietà). L'ho fatto nel mio esempio getter. Fondamentalmente stiamo eseguendo un corpo di funzione per rilevare un valore predefinito ogni volta che il valore non è impostato. Questo è un modello molto comune.
Ma cosa stiamo perdendo e cosa non possiamo fare?
Il fastidio principale, a mio avviso, è che se definisci un getter (come facciamo qui) devi anche definire un setter. [1] È un rumore extra che ingombra il codice.
Un altro fastidio è che dobbiamo ancora inizializzare i valori x
e y
in __init__
. (Beh, ovviamente potremmo aggiungerli usando setattr()
ma questo è più codice extra.)
Terzo, diversamente dall'esempio simile a Java, i getter non possono accettare altri parametri. Ora ti sento già dire, beh, se sta prendendo parametri non è un getter! In senso ufficiale, è vero. Ma in senso pratico non c'è motivo per cui non dovremmo essere in grado di parametrizzare un attributo denominato - come x
- e impostarne il valore per alcuni parametri specifici.
Sarebbe bello se potessimo fare qualcosa del tipo:
e.x[a,b,c] = 10
e.x[d,e,f] = 20
per esempio. Il più vicino che possiamo ottenere è quello di sovrascrivere il compito di implicare una semantica speciale:
e.x = [a,b,c,10]
e.x = [d,e,f,30]
e, naturalmente, assicuriamo che il nostro setter sappia come estrarre i primi tre valori come chiave di un dizionario e impostarne il valore su un numero o qualcosa del genere.
Ma anche se lo facessimo, non potremmo ancora supportarlo con le proprietà perché non c'è modo di ottenere il valore perché non possiamo in alcun modo passare parametri al getter. Quindi abbiamo dovuto restituire tutto, introducendo un'asimmetria.
Il getter / setter in stile Java ci consente di gestirlo, ma siamo tornati a necessitare di getter / setter.
Nella mia mente ciò che vogliamo veramente è qualcosa che catturi i seguenti requisiti:
Gli utenti definiscono un solo metodo per un dato attributo e possono indicare lì se l'attributo è di sola lettura o di lettura-scrittura. Le proprietà non superano questo test se l'attributo è scrivibile.
Non è necessario che l'utente definisca una variabile aggiuntiva alla base della funzione, quindi non abbiamo bisogno del __init__
o setattr
nel codice. La variabile esiste solo per il fatto che abbiamo creato questo nuovo attributo di stile.
Qualsiasi codice predefinito per l'attributo viene eseguito nel corpo del metodo stesso.
Possiamo impostare l'attributo come attributo e fare riferimento come attributo.
Possiamo parametrizzare l'attributo.
In termini di codice, vogliamo un modo per scrivere:
def x(self, *args):
return defaultX()
e poter quindi fare:
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
e così via.
Vogliamo anche un modo per farlo per il caso speciale di un attributo parametrizzabile, ma consentiamo comunque che il caso di assegnazione predefinito funzioni. Vedrai come ho affrontato questo di seguito.
Ora al punto (yay! Il punto!). La soluzione che mi è venuta in mente è la seguente.
Creiamo un nuovo oggetto per sostituire la nozione di proprietà. L'oggetto ha lo scopo di memorizzare il valore di un set di variabili, ma mantiene anche un handle sul codice che sa come calcolare un valore predefinito. Il suo compito è archiviare il set value
o eseguire il method
valore se quel valore non è impostato.
Chiamiamolo 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
Presumo che method
qui sia un metodo di classe, value
è il valore di UberProperty
, e l'ho aggiunto isSet
perché None
potrebbe essere un valore reale e questo ci consente un modo chiaro per dichiarare che non esiste davvero "nessun valore". Un altro modo è una sentinella di qualche tipo.
Questo in pratica ci dà un oggetto che può fare ciò che vogliamo, ma come lo inseriamo nella nostra classe? Bene, le proprietà usano decoratori; perché non possiamo? Vediamo come potrebbe apparire (da qui in poi mi limiterò a usare solo un singolo "attributo" x
).
class Example(object):
@uberProperty
def x(self):
return defaultX()
Questo in realtà non funziona ancora, ovviamente. Dobbiamo implementare uberProperty
e assicurarci che gestisca sia i get che i set.
Cominciamo con get.
Il mio primo tentativo è stato semplicemente quello di creare un nuovo oggetto UberProperty e restituirlo:
def uberProperty(f):
return UberProperty(f)
Ho scoperto rapidamente, ovviamente, che questo non funziona: Python non associa mai il callable all'oggetto e ho bisogno dell'oggetto per chiamare la funzione. Anche la creazione del decoratore nella classe non funziona, poiché anche se ora abbiamo la classe, non abbiamo ancora un oggetto con cui lavorare.
Quindi dovremo essere in grado di fare di più qui. Sappiamo che un metodo deve essere rappresentato solo una volta, quindi andiamo avanti e manteniamo il nostro decoratore, ma modificiamo UberProperty
per memorizzare solo il method
riferimento:
class UberProperty(object):
def __init__(self, method):
self.method = method
Inoltre non è richiamabile, quindi al momento non funziona nulla.
Come completiamo l'immagine? Bene, con cosa finiamo quando creiamo la classe di esempio usando il nostro nuovo decoratore:
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 entrambi i casi torniamo indietro, il UberProperty
che ovviamente non è richiamabile, quindi non è molto utile.
Ciò di cui abbiamo bisogno è un modo per associare dinamicamente l' UberProperty
istanza creata dal decoratore dopo che la classe è stata creata su un oggetto della classe prima che l'oggetto sia stato restituito a quell'utente per l'uso. Uhm, sì, è una __init__
chiamata, amico.
Scriviamo ciò che vogliamo che il nostro risultato di ricerca sia il primo. Stiamo vincolando UberProperty
un'istanza, quindi una cosa ovvia da restituire sarebbe una proprietà limitata. Qui è dove manterremo effettivamente lo stato x
dell'attributo.
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
Ora abbiamo la rappresentazione; come ottenerli su un oggetto? Esistono alcuni approcci, ma il più semplice da spiegare utilizza solo il __init__
metodo per eseguire tale mappatura. Con il tempo __init__
si chiamano i nostri decoratori, quindi basta guardare l'oggetto __dict__
e aggiornare tutti gli attributi in cui il valore dell'attributo è di tipo UberProperty
.
Ora, le proprietà uber sono interessanti e probabilmente vorremmo usarle molto, quindi ha senso creare solo una classe base che faccia questo per tutte le sottoclassi. Penso che tu sappia come verrà chiamata la classe 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)
Aggiungiamo questo, cambiamo il nostro esempio per ereditare da UberObject
, e ...
e = Example()
print e.x -> <__main__.BoundUberProperty object at 0x104604c90>
Dopo aver modificato x
per essere:
@uberProperty
def x(self):
return *datetime.datetime.now()*
Possiamo eseguire un semplice test:
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()
E otteniamo l'output che volevamo:
2013-05-31 00:05:13.985813
2013-05-31 00:05:13.986290
2013-05-31
2013-05-31 00:05:13.986310
(Accidenti, sto lavorando fino a tardi.)
Si noti che ho usato getValue
, setValue
e clearValue
qui. Questo perché non ho ancora collegato i mezzi per restituirli automaticamente.
Ma penso che questo sia un buon posto dove fermarsi per ora, perché mi sto stancando. Puoi anche vedere che la funzionalità di base che volevamo è a posto; il resto è vetrinistica. Importante medicazione dell'usabilità, ma che può attendere fino a quando non ho una modifica per aggiornare il post.
Finirò l'esempio nel prossimo post affrontando queste cose:
Dobbiamo assicurarci che UberObject __init__
sia sempre chiamato da sottoclassi.
- Quindi o forziamo che venga chiamato da qualche parte o ne impediamo l'implementazione.
- Vedremo come farlo con una metaclasse.
Dobbiamo assicurarci di gestire il caso comune in cui qualcuno 'alias' una funzione per qualcos'altro, come:
class Example(object):
@uberProperty
def x(self):
...
y = x
Dobbiamo e.x
tornare e.x.getValue()
per impostazione predefinita.
- Quello che vedremo in realtà è questa è un'area in cui il modello fallisce.
- Si scopre che dovremo sempre usare una chiamata di funzione per ottenere il valore.
- Ma possiamo farlo sembrare una normale chiamata di funzione ed evitare di doverlo usare
e.x.getValue()
. (Fare questo è ovvio, se non l'hai già risolto.)
Dobbiamo supportare l'impostazione e.x directly
, come in e.x = <newvalue>
. Possiamo farlo anche nella classe genitore, ma dovremo aggiornare il nostro __init__
codice per gestirlo.
Infine, aggiungeremo attributi parametrizzati. Dovrebbe essere abbastanza ovvio come faremo anche questo.
Ecco il codice così com'è finora:
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] Potrei essere indietro sul fatto che sia ancora così.