Utilizzo di @property contro getter e setter


728

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 permette di farlo in entrambi i modi. Se progettaste un programma Python, quale approccio usereste e perché?

Risposte:


613

Preferisci proprietà . È quello per cui sono lì.

Il motivo è che tutti gli attributi sono pubblici in Python. Iniziare nomi con un trattino basso o due è solo un avvertimento che l'attributo dato è un dettaglio di implementazione che potrebbe non rimanere lo stesso nelle versioni future del codice. Non ti impedisce di ottenere o impostare quell'attributo. Pertanto, l'accesso agli attributi standard è il normale modo Pythonic di accedere agli attributi.

Il vantaggio delle proprietà è che sono sintatticamente identici all'accesso agli attributi, quindi è possibile passare da uno all'altro senza alcuna modifica al codice client. Potresti persino avere una versione di una classe che utilizza proprietà (diciamo, per codice per contratto o debug) e una versione che non è per la produzione, senza cambiare il codice che la utilizza. Allo stesso tempo, non devi scrivere getter e setter per tutto nel caso in cui potresti aver bisogno di controllare meglio l'accesso in seguito.


90
I nomi degli attributi con un doppio trattino di sottolineatura sono gestiti appositamente da Python; non è solo una semplice convenzione. Vedi docs.python.org/py3k/tutorial/classes.html#private-variables
6502

63
Sono gestiti in modo diverso, ma ciò non ti impedisce di accedervi. PS: AD 30 C0
kindall

4
e perché i caratteri "@" sono brutti nel codice Python e il dereferenziamento di @decorators dà la stessa sensazione di spaghetti-code.
Berry Tsakala

18
Non sono d'accordo In che modo il codice strutturato è uguale al codice spaghetti? Python è un linguaggio bellissimo. Ma sarebbe ancora meglio con un supporto migliore per cose semplici come l'incapsulamento corretto e le classi strutturate.

69
Mentre sono d'accordo nella maggior parte dei casi, fai attenzione a nascondere i metodi lenti dietro un decoratore @property. L'utente della tua API si aspetta che l'accesso alla proprietà si comporti come un accesso variabile e allontanarsi troppo da tale aspettativa può rendere la tua API spiacevole da usare.
defrex,

153

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

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

Questo non è il codice Python.


46
@ 6502, quando dicevi “[…] classi inutili ovunque dove per esempio farebbe una semplice tupla”: il vantaggio di una classe su una tupla è che un'istanza di classe fornisce nomi espliciti per accedere alle sue parti, mentre una tupla no . I nomi sono meglio leggibili ed evitano errori, rispetto alle tuple di sottoscrizione, specialmente quando questo deve essere passato al di fuori del modulo corrente.
Hibou57,

15
@ Hibou57: non sto dicendo che le lezioni sono inutili. Ma a volte una tupla è più che sufficiente. Il problema è, tuttavia, che chi proviene da Java o C ++ non ha altra scelta che creare classi per tutto perché altre possibilità sono semplicemente fastidiose da usare in quei linguaggi. Un altro sintomo tipico della programmazione Java / C ++ utilizzando Python è la creazione di classi astratte e gerarchie di classi complesse senza motivo per cui in Python è possibile utilizzare solo classi indipendenti grazie alla tipizzazione duck.
6502

39
@ Hibou57 per questo puoi anche usare namedtuple: doughellmann.com/PyMOTW/collections/namedtuple.html
hugo24

5
@JonathonReinhart: È nella libreria standard dal 2.6 ... vedi docs.python.org/2/library/collections.html
6502

1
Quando "eventualmente esegui la migrazione a una proprietà, se necessario", è molto probabile che tu rompa il codice usando le tue classi. Le proprietà spesso introducono restrizioni: qualsiasi codice che non si aspettava che queste restrizioni si interrompessero non appena le si introduce.
yaccob,

118

L'uso delle proprietà consente di iniziare con i normali accessi agli attributi e quindi di eseguirne il backup con getter e setter in seguito, se necessario .


3
@GregKrsak Sembra strano perché lo è. La "cosa degli adulti consenzienti" era un meme pitone di prima che le proprietà fossero aggiunte. Era la risposta di magazzino alle persone che si lamentavano della mancanza di modificatori di accesso. Quando sono state aggiunte proprietà, diventa improvvisamente desiderabile l'incapsulamento. La stessa cosa è successa con le classi base astratte. "Python è sempre stato in guerra con la rottura dell'incapsulamento. La libertà è schiavitù. Lambdas dovrebbe adattarsi solo su una linea."
johncip

71

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

A volte c'è bisogno di getter e setter, ma anche allora li "nasconderei" al mondo esterno. Ci sono molti modi per fare questo in Python ( getattr, setattr, __getattribute__, ecc ..., ma 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.


1
@BasicWolf - Ho pensato che fosse implicitamente chiaro che sono sul lato della proprietà della recinzione! :) Ma aggiungo un para alla mia risposta per chiarirlo.
mac,

9
SUGGERIMENTO: la parola "sempre" è un suggerimento che l'autore sta tentando di convincerti con un'affermazione, non una discussione. Così è la presenza del carattere grassetto. (Voglio dire, se vedi CAPS invece, allora - whoa - deve essere giusto.) Guarda, la funzione "proprietà" sembra essere diversa da Java (la nemesi di fatto di Python per qualche motivo), e quindi il pensiero di gruppo della comunità di Python lo dichiara migliore. In realtà, le proprietà violano la regola "Esplicitare è meglio che implicito", ma nessuno vuole ammetterlo. Lo trasformò nel linguaggio, quindi ora viene dichiarato "Pythonic" tramite un argomento tautologico.
Stuart Berg,

3
Nessun sentimento ferito. :-P Sto solo cercando di sottolineare che le convenzioni "Pythonic" sono incoerenti in questo caso: "Explicit è meglio di implicito" è in conflitto diretto con l'utilizzo di a property. ( Sembra un semplice incarico, ma chiama una funzione.) Pertanto, "Pythonic" è essenzialmente un termine senza significato, tranne che per la definizione tautologica: "Le convenzioni Pythonic sono cose che abbiamo definito essere Pythonic."
Stuart Berg,

1
Ora, l' idea di avere una serie di convenzioni che seguono un tema è fantastica . Se esistesse una tale serie di convenzioni, allora potresti usarla come una serie di assiomi per guidare il tuo pensiero, non semplicemente una lunga lista di trucchi da memorizzare, che è significativamente meno utile. Gli assiomi potrebbero essere usati per estrapolare e aiutarti ad affrontare i problemi che nessuno ha ancora visto. È un peccato che la propertyfunzione minacci di rendere quasi inutile l'idea degli assiomi Pythonic. Quindi ci rimane solo una lista di controllo.
Stuart Berg,

1
Non sono d'accordo Preferisco le proprietà nella maggior parte delle situazioni, ma quando si desidera sottolineare che l'impostazione di qualcosa ha effetti collaterali diversi dalla modifica selfdell'oggetto , possono essere utili setter espliciti. Ad esempio, user.email = "..."non sembra che possa sollevare un'eccezione perché sembra solo impostare un attributo, mentre user.set_email("...")chiarisce che potrebbero esserci effetti collaterali come le eccezioni.
bluenote10,

65

[ 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 defaultXe defaultYnel __init__metodo dell'oggetto . Il motivo è che nel nostro caso voglio presumere che i someDefaultComputationmetodi 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 xe yin __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 setattrnel 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 valueo eseguire il methodvalore 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 methodqui sia un metodo di classe, valueè il valore di UberProperty, e l'ho aggiunto isSetperché Nonepotrebbe 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 uberPropertye 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 UberPropertyper memorizzare solo il methodriferimento:

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 UberPropertyche ovviamente non è richiamabile, quindi non è molto utile.

Ciò di cui abbiamo bisogno è un modo per associare dinamicamente l' UberPropertyistanza 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 UberPropertyun'istanza, quindi una cosa ovvia da restituire sarebbe una proprietà limitata. Qui è dove manterremo effettivamente lo stato xdell'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 xper 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, setValuee clearValuequi. 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.xtornare 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ì.


53
Sì, questo è "tldr". Puoi riassumere per favore cosa stai cercando di fare qui?
poolie

9
@Adam return self.x or self.defaultX()questo è un codice pericoloso. Cosa succede quando self.x == 0?
Kelly Thomas,

Cordiali saluti, puoi farlo in modo da poter parametrizzare il getter, tipo di. Implicherebbe rendere la variabile una classe personalizzata, di cui hai ignorato il __getitem__metodo. Sarebbe strano però, dato che avresti un pitone completamente non standard.
sarà il

2
@KellyThomas Sto solo cercando di rendere semplice l'esempio. Per farlo bene, devi creare ed eliminare del tutto la voce x dict , perché anche un valore Nessuno potrebbe essere stato impostato in modo specifico. Ma sì, hai assolutamente ragione, questo è qualcosa che dovresti considerare in un caso d'uso di produzione.
Adam Donahue,

Getter simil-Java ti permettono di fare esattamente lo stesso calcolo, vero?
qed

26

Penso che entrambi abbiano il loro posto. Un problema con l'utilizzo @propertyè che è difficile estendere il comportamento di getter o setter in sottoclassi usando meccanismi di classe standard. Il problema è che le effettive 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 C.p.fgete C.p.fset, ma non è possibile utilizzare facilmente le normali funzionalità di ereditarietà del metodo (ad es. super) per estenderle. 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'uso di super () è, tuttavia, piuttosto ingombrante, poiché la proprietà deve essere ridefinita e per ottenere una copia non associata di p è necessario utilizzare il meccanismo super (cls, cls) leggermente contro-intuitivo.


20

L'uso delle proprietà è per me più intuitivo e si adatta meglio alla maggior parte del codice.

confrontando

o.x = 5
ox = o.x

vs.

o.setX(5)
ox = o.getX()

è abbastanza ovvio per me che è più facile da leggere. Anche le proprietà consentono variabili private molto più facili.


12

Preferirei non usare nessuno dei due 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 prevede 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 chiamate per essere gestita correttamente o comportare la presentazione di un traceback molto inutile all'utente del programma (che purtroppo è troppo comune nel mondo di Python e Java ).

Eviterei anche di usare getter e setter:

  • perché definirli per tutte le proprietà in anticipo richiede molto tempo,
  • rende inutilmente più lunga la quantità di codice, il che rende più difficile la comprensione e la gestione del codice,
  • se le definissi per 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 luoghi 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 abbastanza semplici e trasparenti da non averne bisogno.


3
@ user2239734 Penso che tu fraintenda il concetto di proprietà. Sebbene sia possibile convalidare il valore durante l'impostazione della proprietà, non è necessario farlo. Puoi avere sia proprietà che un validate()metodo in una classe. Una proprietà viene semplicemente utilizzata quando si ha una logica complessa dietro una semplice obj.x = yassegnazione, e dipende da quale sia la logica.
Zaur Nasibov,

12

Sento che le proprietà consistono nel farti avere il sovraccarico di scrivere getter e setter solo quando ne hai davvero bisogno.

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

In Python, le persone si prendono effettivamente cura di quel tipo di spese generali, in modo da poter abbracciare la seguente pratica:

  • Non usare getter e setter all'inizio, quando non sono necessari
  • Utilizzare @propertyper implementarli senza modificare la sintassi del resto del codice.

1
"e nota che il 70% delle volte non viene mai sostituito da una logica non banale." - è un numero piuttosto specifico, viene da qualche parte, o lo intendi come una cosa "a grande maggioranza" fatta a mano (non sto facendo il facet, se c'è uno studio che quantifica quel numero, sarei sinceramente interessato a leggerlo)
Adam Parkin,

1
Oh no scusa. Sembra che ho qualche studio per eseguire il backup di questo numero, ma intendevo solo "il più delle volte".
fulmicoton,

7
Non è che le persone si preoccupino dell'overhead, è che in Python puoi cambiare dall'accesso diretto ai metodi di accesso senza cambiare il codice client, quindi non hai nulla da perdere esponendo direttamente le proprietà all'inizio.
Neil G,

10

Sono sorpreso che nessuno abbia menzionato che le proprietà sono metodi associati di una classe descrittiva, Adam Donohue e NeilenMarais trovano esattamente questa idea nei loro post: che getter e setter sono funzioni e possono essere usati per:

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

Ciò presenta un modo intelligente per nascondere i dettagli dell'implementazione e la codifica del codice come espressione regolare, digitare cast, provare .. ad eccezione di blocchi, asserzioni o valori calcolati.

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

Se si pensa alle proprietà solo come a qualche triste traccia di un linguaggio di Bondage e Disciplina (cioè Java) manca il punto di descrittori.


6

In progetti complessi preferisco usare proprietà (o getter) di sola lettura con la funzione di impostazione esplicita:

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

def set_my_attr(self, value):
    ...

Nei progetti di lunga durata il debug e il refactoring richiedono più tempo rispetto alla scrittura del codice stesso. Esistono diversi aspetti negativi dell'utilizzo @property.setterche rendono ancora più difficile il debug:

1) python consente di creare nuovi attributi per un oggetto esistente. Ciò rende molto difficile tracciare un seguente errore di stampa:

my_object.my_atttr = 4.

Se il tuo oggetto è un algoritmo complicato, trascorrerai un bel po 'di tempo cercando di scoprire perché non converge (nota una' t 'in più nella riga sopra)

2) il setter a volte potrebbe evolversi in un metodo complicato e lento (es. Colpire un database). Sarebbe piuttosto difficile per un altro sviluppatore capire perché la seguente funzione è molto lenta. Potrebbe dedicare molto tempo al do_something()metodo di profilazione , mentre in my_object.my_attr = 4.realtà è la causa del rallentamento:

def slow_function(my_object):
    my_object.my_attr = 4.
    my_object.do_something()

5

Sia @propertyi getter che i setter tradizionali hanno i loro vantaggi. Dipende dal tuo caso d'uso.

Vantaggi di @property

  • Non è necessario modificare l'interfaccia mentre si modifica l'implementazione dell'accesso ai dati. Quando il progetto è piccolo, probabilmente si desidera utilizzare l'accesso diretto agli attributi per accedere a un membro della classe. Ad esempio, supponiamo che tu abbia un oggetto foodi tipo Foo, che ha un membro num. Quindi puoi semplicemente ottenere questo membro con num = foo.num. Man mano che il tuo progetto cresce, potresti pensare che ci debbano essere dei controlli o dei debug sul semplice accesso agli attributi. Quindi puoi farlo con un @property all'interno della classe. L'interfaccia di accesso ai dati rimane la stessa in modo che non sia necessario modificare il codice client.

    Citato da PEP-8 :

    Per semplici attributi di dati pubblici, è meglio esporre solo il nome dell'attributo, senza complicati metodi di accesso / mutatore. Tieni presente che Python fornisce un percorso semplice per il miglioramento futuro, se ritieni che un semplice attributo di dati debba aumentare il comportamento funzionale. In tal caso, utilizzare le proprietà per nascondere l'implementazione funzionale dietro la sintassi di accesso agli attributi di dati semplici.

  • L'uso @propertyper l'accesso ai dati in Python è considerato come Pythonic :

    • Può rafforzare la tua autoidentificazione come programmatore Python (non Java).

    • Può aiutare il tuo colloquio di lavoro se il tuo intervistatore ritiene che i getter e setter in stile Java siano anti-schemi .

Vantaggi dei getter e setter tradizionali

  • Getter e setter tradizionali consentono un accesso ai dati più complicato rispetto al semplice accesso agli attributi. Ad esempio, quando si imposta un membro della classe, a volte è necessario un flag che indica dove si desidera forzare questa operazione anche se qualcosa non sembra perfetto. Sebbene non sia ovvio come aumentare l'accesso diretto di un membro come foo.num = num, puoi facilmente aumentare il setter tradizionale con un forceparametro aggiuntivo :

    def Foo:
        def set_num(self, num, force=False):
            ...
  • Getter e setter tradizionali rendono esplicito che l'accesso di un membro della classe avviene tramite un metodo. Questo significa:

    • Ciò che ottieni come risultato potrebbe non essere lo stesso di ciò che è memorizzato esattamente in quella classe.

    • Anche se l'accesso appare come un semplice accesso agli attributi, le prestazioni possono variare notevolmente da questo.

    A meno che gli utenti della tua classe non si aspettino di @propertynascondersi dietro ogni dichiarazione di accesso agli attributi, rendere esplicite queste cose può aiutare a ridurre al minimo le sorprese degli utenti della tua classe.

  • Come menzionato da @NeilenMarais e in questo post , estendere i getter e setter tradizionali in sottoclassi è più facile che estendere le proprietà.

  • Getter e setter tradizionali sono stati ampiamente utilizzati per lungo tempo in diverse lingue. Se nella tua squadra ci sono persone di diversa estrazione, sembrano più familiari di @property. Inoltre, man mano che il tuo progetto cresce, se potresti dover migrare da Python in un'altra lingua che non ha @property, l'uso di getter e setter tradizionali renderebbe la migrazione più fluida.

Avvertenze

  • @propertyi getter e setter tradizionali rendono privato il membro della classe, anche se si utilizza il doppio trattino di sottolineatura prima del nome:

    class Foo:
        def __init__(self):
            self.__num = 0
    
        @property
        def num(self):
            return self.__num
    
        @num.setter
        def num(self, num):
            self.__num = num
    
        def get_num(self):
            return self.__num
    
        def set_num(self, num):
            self.__num = num
    
    foo = Foo()
    print(foo.num)          # output: 0
    print(foo.get_num())    # output: 0
    print(foo._Foo__num)    # output: 0

2

Ecco alcuni estratti di "Effective Python: 90 modi specifici per scrivere meglio Python" (Libro straordinario. Lo consiglio vivamente).

Cose da ricordare

✦ Definire nuove interfacce di classe usando semplici attributi pubblici ed evitare di definire metodi setter e getter.

✦ Utilizzare @property per definire un comportamento speciale quando si accede agli attributi sugli oggetti, se necessario.

✦ Segui la regola del minimo stupore ed evita strani effetti collaterali nei tuoi metodi @property.

✦ Assicurarsi che i metodi @property siano veloci; per un lavoro lento o complesso, che coinvolge in particolare I / O o causa di effetti collaterali, utilizzare invece metodi normali.

Un uso avanzato ma comune di @property è la transizione di quello che una volta era un semplice attributo numerico in un calcolo al volo. Questo è estremamente utile perché ti consente di migrare tutto l'utilizzo esistente di una classe per avere nuovi comportamenti senza richiedere la riscrittura di nessuno dei siti di chiamata (il che è particolarmente importante se c'è un codice di chiamata che non controlli). @property fornisce anche un importante punto fermo per migliorare le interfacce nel tempo.

Mi piace in particolare @property perché ti consente di fare progressi progressivi verso un modello di dati migliore nel tempo.
@property è uno strumento che ti aiuta ad affrontare i problemi che incontrerai nel codice del mondo reale. Non abusare. Quando ti ritrovi a estendere ripetutamente i metodi @property, è probabilmente il momento di riformattare la tua classe invece di approfondire ulteriormente il design scadente del tuo codice.

✦ Utilizzare @property per dare nuove funzionalità agli attributi di istanza esistenti.

✦ Fare progressi progressivi verso modelli di dati migliori utilizzando @property.

✦ Valuta di effettuare il refactoring di una classe e di tutti i siti di chiamata quando ti ritrovi a utilizzare @property in modo eccessivo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.