property - python repr




Usando @property contro getter e setter (8)

Ecco una domanda di design specifica per Python:

class MyClass(object):
    ...
    def get_my_attr(self):
        ...

    def set_my_attr(self, value):
        ...

e

class MyClass(object):
    ...        
    @property
    def my_attr(self):
        ...

    @my_attr.setter
    def my_attr(self, value):
        ...

Python ci consente di farlo in entrambi i modi. Se dovessi progettare un programma Python, quale approccio useresti e perché?


In Python non usi getter o setter o proprietà solo per il gusto di farlo. Prima devi solo usare gli attributi e poi, solo se necessario, migrare alla proprietà senza dover cambiare il codice usando le tue classi.

Esiste un sacco di codice con estensione .py che usa getter e setter e classi di ereditarietà e inutili ovunque, ad esempio una semplice tupla, ma è il codice di persone che scrivono in C ++ o Java usando Python.

Quello non è il codice Python.



La risposta breve è: proprietà vince a mani basse. Sempre.

A volte c'è bisogno di getter e setter, ma anche allora, li "nascondo" al mondo esterno. Ci sono molti modi per farlo in Python ( getattr , setattr , __getattribute__ , etc ..., ma uno molto conciso e pulito è:

def set_email(self, value):
    if '@' not in value:
        raise Exception("This doesn't look like an email address.")
    self._email = value

def get_email(self):
    return self._email

email = property(get_email, set_email)

Ecco un breve articolo che introduce l'argomento di getter e setter in Python.


Mi sento come se le proprietà ti permettessero di ottenere il sovraccarico di scrivere getter e setter solo quando effettivamente ne hai bisogno.

La cultura della programmazione Java consiglia vivamente di non dare mai accesso alle proprietà e, invece, passare attraverso getter e setter e solo quelli effettivamente necessari. È un po 'prolisso scrivere sempre questi ovvi pezzi di codice e notare che il 70% delle volte non viene mai sostituito da una logica non banale.

In Python, le persone in realtà si preoccupano per quel tipo di overhead, in modo da poter abbracciare la seguente pratica:

  • Non utilizzare getter e setter in un primo momento, se non necessario
  • Utilizza @property per implementarli senza modificare la sintassi del resto del codice.

Penso che entrambi abbiano il loro posto. Un problema con l'uso di @property è che è difficile estendere il comportamento di getter o setter in sottoclassi usando meccanismi di classe standard. Il problema è che le funzioni getter / setter sono nascoste nella proprietà.

Puoi effettivamente ottenere le funzioni, ad es. Con

class C(object):
    _p = 1
    @property
    def p(self):
        return self._p
    @p.setter
    def p(self, val):
        self._p = val

è possibile accedere alle funzioni getter e setter come Cpfget e Cpfset , ma non è possibile utilizzare facilmente l'ereditarietà del metodo normale (ad es. super) per estenderli. Dopo aver scavato nella complessità di super, puoi davvero usare super in questo modo:

# Using super():
class D(C):
    # Cannot use super(D,D) here to define the property
    # since D is not yet defined in this scope.
    @property
    def p(self):
        return super(D,D).p.fget(self)

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for D'
        super(D,D).p.fset(self, val)

# Using a direct reference to C
class E(C):
    p = C.p

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for E'
        C.p.fset(self, val)

L'utilizzo di super () è, tuttavia, abbastanza complesso, poiché la proprietà deve essere ridefinita, e devi usare il meccanismo leggermente super-intuitivo (cls, cls) per ottenere una copia non associata di p.


Preferirei non usare né nella maggior parte dei casi. Il problema con le proprietà è che rendono la classe meno trasparente. Soprattutto, questo è un problema se si dovesse sollevare un'eccezione da un setter. Ad esempio, se si dispone di una proprietà Account.email:

class Account(object):
    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, value):
        if '@' not in value:
            raise ValueError('Invalid email address.')
        self._email = value

quindi l'utente della classe non si aspetta che l'assegnazione di un valore alla proprietà possa causare un'eccezione:

a = Account()
a.email = 'badaddress'
--> ValueError: Invalid email address.

Di conseguenza, l'eccezione potrebbe non essere gestita e propagarsi troppo in alto nella catena di chiamata per essere gestita correttamente, o portare a un traceback molto inutile presentato all'utente del programma (che è tristemente troppo comune nel mondo di Python e Java) ).

Eviterei anche l'uso di getter e setter:

  • perché definirli in anticipo per tutte le proprietà richiede molto tempo,
  • rende la quantità di codice inutilmente più lunga, il che rende più difficile la comprensione e la manutenzione del codice,
  • se le definissi come proprietà solo se necessario, l'interfaccia della classe cambierebbe, danneggiando tutti gli utenti della classe

Invece di proprietà e getter / setter preferisco fare la logica complessa in posti ben definiti come in un metodo di validazione:

class Account(object):
    ...
    def validate(self):
        if '@' not in self.email:
            raise ValueError('Invalid email address.')

o un metodo Account.save simile.

Nota che non sto cercando di dire che non ci sono casi in cui le proprietà sono utili, solo che potresti stare meglio se puoi rendere le tue lezioni semplici e trasparenti abbastanza da non averne bisogno.


Sono sorpreso che nessuno abbia menzionato che le proprietà sono metodi vincolati di una classe descrittore, e prendono esattamente questa idea nei loro post - che i getter e i setter sono funzioni e possono essere usati per:

  • convalidare
  • alterare i dati
  • tipo di anatra (tipo di coercizione ad un altro tipo)

Questo presenta un modo intelligente per nascondere dettagli di implementazione e code cruft come espressioni regolari, cast di tipi, try ... eccetto blocchi, asserzioni o valori calcolati.

In generale, fare il CRUD su un oggetto può spesso essere abbastanza banale ma considerare l'esempio di dati che verranno mantenuti in un database relazionale. Gli ORM possono nascondere i dettagli di implementazione di particolari vernacoli SQL nei metodi associati a fget, fset, fdel definiti in una classe di proprietà che gestirà le terribili if .. elif .. else ladder che sono così brutte nel codice OO - esponendo il semplice e elegante self.variable = something e self.variable = something i dettagli per lo sviluppatore utilizzando l'ORM.

Se si considerano le proprietà solo come alcune deprimenti vestigia di un linguaggio Bondage e Discipline (cioè Java), mancano il punto dei descrittori.


[ TL; DR? Puoi saltare alla fine per un esempio di codice .]

In realtà preferisco usare un linguaggio diverso, che è un po 'complicato da usare come singolo, ma è bello se hai un caso d'uso più complesso.

Prima un po 'di storia.

Le proprietà sono utili in quanto ci consentono di gestire entrambe le impostazioni e ottenere valori in modo programmatico, ma consentono comunque di accedere agli attributi come attributi. Possiamo trasformare "ottiene" in "computazioni" (essenzialmente) e possiamo trasformare "insiemi" in "eventi". Quindi diciamo che abbiamo la seguente classe, che ho codificato con getter e setter Java-like.

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()

Ci si potrebbe chiedere perché non ho chiamato defaultX e defaultY nel metodo __init__ dell'oggetto. La ragione è che per il nostro caso voglio assumere che i metodi someDefaultComputation restituiscano valori che variano nel tempo, ad esempio un timestamp, e ogni volta che x (o y ) non è impostato (dove, per lo scopo di questo esempio, "non impostato" significa "impostato su Nessuno") Voglio il valore del calcolo predefinito di x 's (s).

Quindi questo è zoppo per una serie di ragioni sopra descritte. 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 riferirci a questi attributi come attributi anche se, dietro le quinte, finiamo con i metodi di esecuzione.

Ovviamente il vero potere delle proprietà è che generalmente vogliamo che questi metodi facciano qualcosa oltre a ottenere e impostare i valori (altrimenti non ha senso usare le proprietà). Ho fatto questo nel mio esempio getter. Fondamentalmente stiamo eseguendo un corpo di funzione per raccogliere 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 principale fastidio, a mio avviso, è che se si definisce un getter (come facciamo qui) bisogna anche definire un setter. [1] Questo è un rumore in più che ingombra il codice.

Un altro fastidio è che dobbiamo ancora inizializzare i valori y in __init__ . (Beh, ovviamente potremmo aggiungerli usando setattr() ma questo è più codice extra.)

In terzo luogo, a differenza dell'esempio Java, i getter non possono accettare altri parametri. Ora posso sentirti dire già, beh, se sta prendendo i parametri non è un getter! In un senso ufficiale, è vero. Ma in senso pratico non c'è ragione per cui non dovremmo essere in grado di parametrizzare un attributo chiamato - come x - e impostare il suo valore per alcuni parametri specifici.

Sarebbe bello se potessimo fare qualcosa come:

e.x[a,b,c] = 10
e.x[d,e,f] = 20

per esempio. Il più vicino che possiamo ottenere è quello di scavalcare il compito per implicare alcune semantiche speciali:

e.x = [a,b,c,10]
e.x = [d,e,f,30]

e ovviamente assicuriamo che il nostro setter sappia come estrarre i primi tre valori come chiave per un dizionario e impostarne il valore su un numero o qualcosa.

Ma anche se lo facessimo, non potremmo ancora supportarlo con le proprietà perché non c'è modo di ottenere il valore perché non possiamo passare affatto i 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 richiedere getter / setter.

Nella mia mente ciò che vogliamo veramente è qualcosa che cattura i seguenti requisiti:

  • Gli utenti definiscono un solo metodo per un determinato attributo e possono indicare lì se l'attributo è di sola lettura o di lettura-scrittura. Le proprietà falliscono questo test se l'attributo è scrivibile.

  • Non è necessario che l'utente definisca una variabile extra alla base della funzione, quindi non abbiamo bisogno di __init__ o setattr nel codice. La variabile esiste semplicemente dal fatto che abbiamo creato questo attributo di nuovo stile.

  • Qualsiasi codice predefinito per l'attributo viene eseguito nel corpo del metodo stesso.

  • Possiamo impostare l'attributo come attributo e riferirlo come un attributo.

  • Possiamo parametrizzare l'attributo.

In termini di codice, vogliamo un modo per scrivere:

def x(self, *args):
    return defaultX()

e essere in grado di fare quindi:

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 permetti comunque che il caso di assegnazione predefinito funzioni. Vedrai come ho affrontato questo sotto.

Ora al punto (yay! Il punto!). La soluzione per la quale sono arrivato è la seguente.

Creiamo un nuovo oggetto per sostituire la nozione di una proprietà. L'oggetto è destinato a memorizzare il valore di una variabile impostata su di esso, ma mantiene anche un handle sul codice che sa come calcolare un valore predefinito. Il suo compito è quello di memorizzare il value impostato o di eseguire il method se tale valore non è impostato.

Chiamiamolo 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 il method qui sia un metodo di classe, value è il valore di UberProperty e ho aggiunto isSet perché None potrebbe essere un valore reale e questo ci consente di dichiarare che esiste un "valore senza valore". Un altro modo è una sentinella di qualche tipo.

Questo fondamentalmente ci dà un oggetto che può fare ciò che vogliamo, ma come lo mettiamo effettivamente nella nostra classe? Bene, le proprietà usano decoratori; perché non possiamo? Vediamo come potrebbe apparire (da qui in avanti mi limiterò a usare solo un singolo 'attributo', x ).

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

Questo in realtà non funziona ancora, naturalmente. Dobbiamo implementare uberProperty e assicurarci che gestisca sia uberProperty che sets.

Iniziamo con ottiene.

Il mio primo tentativo è stato semplicemente creare un nuovo oggetto UberProperty e restituirlo:

def uberProperty(f):
    return UberProperty(f)

Ho scoperto subito, naturalmente, che questo non funziona: Python non lega mai il callable all'oggetto e ho bisogno dell'oggetto per chiamare la funzione. Anche la creazione del decoratore nella classe non funziona, poiché sebbene ora abbiamo la classe, non abbiamo ancora un oggetto con cui lavorare.

Quindi avremo bisogno di essere in grado di fare di più qui. Sappiamo che un metodo deve essere rappresentato solo una volta, quindi andiamo avanti e mantieni il nostro decoratore, ma modifica UberProperty per memorizzare solo il riferimento al method :

class UberProperty(object):

    def __init__(self, method):
        self.method = method

Non è nemmeno chiamabile, quindi al momento non funziona nulla.

Come completiamo l'immagine? Bene, a cosa ci ritroviamo 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 a UberProperty che, ovviamente, non è richiamabile, quindi non è molto utile.

Ciò di cui abbiamo bisogno è un modo per associare dinamicamente l'istanza UberProperty creata dal decoratore dopo che la classe è stata creata su un oggetto della classe prima che quell'oggetto sia stato restituito all'utente per l'uso. Ehm, si, è una chiamata __init__ , amico.

Scriviamo ciò che vogliamo che il risultato della nostra ricerca sia il primo. UberProperty una proprietà UberProperty a un'istanza, quindi una cosa ovvia da restituire sarebbe una proprietà BoundUberProperty. Qui è dove manterremo lo stato per l'attributo x .

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 siamo la rappresentazione; come ottenerli su un oggetto? Esistono alcuni approcci, ma quello più semplice da spiegare utilizza semplicemente il metodo __init__ per eseguire tale mappatura. Quando __init__ viene chiamato, i nostri decoratori hanno eseguito, quindi è sufficiente esaminare l' __dict__ dell'oggetto e aggiornare gli attributi in cui il valore dell'attributo è di tipo UberProperty .

Ora, le proprietà uber sono fantastiche e probabilmente vorremmo usarle molto, quindi ha senso creare una classe base che faccia questo per tutte le sottoclassi. Penso che tu sappia come si chiamerà 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 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.)

Nota che ho usato getValue , setValue e clearValue qui. Questo perché non ho ancora collegato i mezzi per farli tornare automaticamente.

Ma penso che questo sia un buon posto dove fermarsi per ora, perché mi sto stancando. Puoi anche vedere che la funzionalità principale che volevamo è a posto; il resto è la vetrinistica. Importante window dress per l'usabilità, ma questo può aspettare finché non ho una modifica per aggiornare il post.

Finirò l'esempio nel prossimo post affrontando queste cose:

  • Dobbiamo assicurarci che l'__init__ di UberObject sia sempre chiamato da sottoclassi.

    • Quindi o lo forziamo a chiamarlo da qualche parte o impediamo che venga implementato.
    • Vedremo come farlo con un metaclass.
  • Dobbiamo assicurarci di gestire il caso comune in cui qualcuno "alias" una funzione per qualcos'altro, come ad esempio:

      class Example(object):
          @uberProperty
          def x(self):
              ...
    
          y = x
    
  • Abbiamo bisogno di ex per restituire exgetValue() per impostazione predefinita.

    • Quello che vedremo in realtà è che questa è un'area in cui il modello fallisce.
    • Risulta che avremo sempre bisogno di utilizzare una chiamata di funzione per ottenere il valore.
    • Ma possiamo farlo sembrare una normale chiamata di funzione ed evitare di dover usare exgetValue() . (Fare questo è ovvio, se non lo hai già risolto.)
  • Dobbiamo supportare l'impostazione ex directly , come in ex = <newvalue> . Possiamo farlo anche nella classe genitore, ma dovremo aggiornare il nostro codice __init__ per gestirlo.

  • Infine, aggiungeremo attributi parametrizzati. Dovrebbe essere abbastanza ovvio come lo faremo anche noi.

Ecco il codice così com'è fino ad ora:

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 in ritardo sulla questione se sia ancora così.





getter-setter