2012-03-29 7 views
24

Ho molte piccole classi diverse che hanno ciascuno alcuni campi, ad es. questo:Come posso esporre i campi di sola lettura dalle classi Python?

class Article: 
    def __init__(self, name, available): 
     self.name = name 
     self.available = available 

Qual è il modo più semplice e/o più idiomatico per rendere il campo name sola lettura, in modo che

a = Article("Pineapple", True) 
a.name = "Banana" # <-- should not be possible 

non è più possibile?

Ecco quello che ho considerato finora:

  1. Utilizzare un getter (! Ugh).

    class Article: 
        def __init__(self, name, available): 
         self._name = name 
         self.available = available 
    
        def name(self): 
         return self._name 
    

    Ugly, non pythonic - e un sacco di codice boilerplate per scrivere (soprattutto se ho più campi per rendere di sola lettura). Tuttavia, fa il lavoro ed è facile capire perché sia ​​così.

  2. Uso __setattr__:

    class Article: 
        def __init__(self, name, available): 
         self.name = name 
         self.available = available 
    
        def __setattr__(self, name, value): 
         if name == "name": 
          raise Exception("%s property is read-only" % name) 
         self.__dict__[name] = value 
    

    sembra piuttosto sul lato chiamante, sembra essere il modo idiomatico per fare il lavoro - ma purtroppo ho molte classi con pochi campi per fare solo ogni lettura. Quindi dovrei aggiungere un'implementazione __setattr__ a tutti loro. O usare una sorta di mixin forse? In ogni caso, dovrei decidere come comportarmi nel caso in cui un client tenti di assegnare un valore a un campo di sola lettura. Fai qualche eccezione, immagino, ma quale?

  3. Utilizzare una funzione di utilità per definire automaticamente le proprietà (e facoltativamente i getter). Questo è fondamentalmente la stessa idea (1), tranne che non scrivo i getter esplicitamente, ma piuttosto fare qualcosa di simile

    class Article: 
        def __init__(self, name, available): 
         # This function would somehow give a '_name' field to self 
         # and a 'name()' getter to the 'Article' class object (if 
         # necessary); the getter simply returns self._name 
         defineField(self, "name") 
         self.available = available 
    

    Il rovescio della medaglia è che io non so nemmeno se questo è possibile (o come implementarlo) poiché non ho familiarità con la generazione del codice di runtime in Python. :-)

Finora, (2) sembra essere più promettente per me, tranne per il fatto che avrò bisogno __setattr__ definizioni a tutte le mie classi. Vorrei che ci fosse un modo per "annotare" i campi in modo che ciò avvenga automaticamente. Qualcuno ha un'idea migliore?

Per quello che vale, sto cantando Python 2.6.

AGGIORNAMENTO: Grazie per tutte le risposte interessanti! Per ora, ho questo:

def ro_property(o, name, value): 
    setattr(o.__class__, name, property(lambda o: o.__dict__["_" + name])) 
    setattr(o, "_" + name, value) 

class Article(object): 
    def __init__(self, name, available): 
     ro_property(self, "name", name) 
     self.available = available 

Questo sembra funzionare abbastanza bene. Le uniche modifiche necessarie per la classe originale sono

  • ho bisogno di ereditare object (che non è una cosa così stupida in ogni caso, credo)
  • ho bisogno di cambiare self._name = name-ro_property(self, "name", name).

Questo per me è abbastanza bello - qualcuno può vedere un rovescio della medaglia?

+2

Il tuo 'ro_property' è troppo complicato: la risposta di Sven o di Chris è la strada da percorrere. –

+0

@EthanFurman è complicato, ma forse è il prezzo che devi pagare se vuoi mantenere le modifiche apportate alle classi esistenti al minimo? –

+0

Non riesco a ricordare quando ho scritto per ultimo l'attributo di un'istanza (al di fuori dei metodi dell'istanza), tranne quando misero in patch il codice di qualcun altro. Gli attributi di istanza non sono quasi sempre destinati alla sola lettura pratica? Gli attributi di istanza sono in genere impostati in '__init__' e non modificati in seguito, a meno che non si chiami un metodo sull'istanza. Credo che consideri questo stile di codifica appropriato. –

risposta

37

userei property come decoratore per gestire il tuo getter per name (vedi l'esempio per la classe Parrot nella documentazione). Usa, per esempio, qualcosa come:

class Article(object): 
    def __init__(self, name, available): 
     self._name = name 
     self.available = available 

    @property 
    def name(self): 
     return self._name 

Se non si definisce il setter per il name proprietà (utilizzando il decoratore x.setter intorno una funzione) questo genera un AttributeError quando si cerca di ripristinare name.

Nota: Devi usare classi di nuovo stile di Python (vale a dire in Python 2.6 si deve ereditare da object) oggetti da funzionare correttamente. Questo non è il caso secondo @SvenMarnach.

+2

Questo sembra carino dal punto di vista del chiamante, ma sembra ancora un piatto di caldaia quando scrive effettivamente le classi. Sarebbe bello se potessi fare qualcosa come '@property self_name = name' o così nel costruttore in modo che Python definisse automaticamente il getter per me. Così com'è, questo codice è più carino per il chiamante, ma ancora più codice da scrivere per l'autore della classe piuttosto che un semplice getter (la linea aggiuntiva per '@ property'). –

+0

Qualcosa come '@property self._name = name' sarebbe bello, ma i decoratori lavorano su funzioni piuttosto che su oggetti. È una linea in più sulla tua idea getter, ma questo è il modo idiomatico di farlo, e il modo in cui la documentazione di Python dimostra la creazione di variabili di classe di sola lettura. In definitiva, avrai sempre bisogno di un codice boilerplate per ridefinire il comportamento predefinito delle variabili di classe. – Chris

+1

[Le proprietà funzionano anche sulle classi vecchio stile.] (Http://ideone.com/wgXAX) Il descrittore stesso, cioè la classe 'property' stessa, deve essere una classe di nuovo stile, ma lo è. La documentazione di Python è un po 'fuorviante in questo senso. –

2

vorrei bastone con l'opzione 1, ma raffinato in modo da utilizzare Python proprietà:

class Article 
    def get_name(self): 
     return self.__name 

    name = property(get_name) 
+4

I caratteri di sottolineatura doppio servono per impedire che le sottoclassi superino accidentalmente i campi. Per i normali campi "privati" il singolo carattere di sottolineatura è corretto. Inoltre, è meglio usare 'property' come decoratore. – agf

+1

Hm, suona interestinig (specialmente se potessi passare un lambda a 'property' ...), ma non riesco a farlo funzionare. 'get_name' sarebbe un metodo globale e l'assegnazione' name' verrebbe eseguita nel costruttore? –

+1

Grazie agf. Il codice sopra deve essere inserito all'interno della classe, non a livello globale. – Pinch

4

Si noti che è sempre possibile modificare gli attributi di un oggetto in Python - non ci sono variabili veramente private in Python. È solo che alcuni approcci lo rendono un po 'più difficile. Ma un codificatore determinato può sempre cercare e modificare il valore di un attributo. Per esempio, posso sempre modificare il tuo __setattr__ se voglio ...

Per ulteriori informazioni, vedere Section 9.6 di The Python Tutorial. Python usa il nome mangling quando gli attributi sono prefissati con __, quindi il nome effettivo in fase di runtime è diverso, ma è comunque possibile derivare ciò che quel nome in fase di esecuzione è (e quindi modificare l'attributo).

8

base nella risposta di Chris, ma forse più divinatorio:

def ro_property(field): 
    return property(lambda self : self.__dict__[field]) 

class Article(object): 
    name = ro_property('_name') 

    def __init__(self): 
     self._name = "banana" 

Se il tentativo di modificare la proprietà intende sollevare un AttributeError.

a = Article() 
print a.name # -> 'banana' 
a.name = 'apple' # -> AttributeError: can't set attribute 

UPDATE: Circa la risposta aggiornato, il (piccolo) problema che vedo è che si sta modificando la definizione della proprietà nella classe ogni volta che si crea un'istanza. E non penso sia una buona idea. Ecco perché ho messo la ro_property chiamata esterna della funzione __init__

Che dire ?:

def ro_property(name): 
    def ro_property_decorator(c): 
     setattr(c, name, property(lambda o: o.__dict__["_" + name])) 
     return c 
    return ro_property_decorator 

@ro_property('name') 
@ro_property('other') 
class Article(object): 
    def __init__(self, name): 
     self._name = name 
     self._other = "foo" 

a = Article("banana") 
print a.name # -> 'banana' 
a.name = 'apple' # -> AttributeError: can't set attribute 

decoratori di classe sono di fantasia!

+0

Mi piace come questo riduce la quantità di modifiche da apportare alle classi. –

+0

Grazie mille per questa risposta, per favore vedi la mia risposta aggiornata per quello che ora ho. Raggiunge solo ciò di cui ho bisogno, penso, e la chagne alla classe originale è solo una riga (invece di 'self._prop = prop' ora faccio' ro_property (self, "prop", prop) '). –

+0

Per favore, vedi la mia risposta aggiornata con una nuova proposta. – rodrigo

12

Come indicato in altre risposte, l'utilizzo di una proprietà è la via da seguire per gli attributi di sola lettura. La soluzione in Chris' answer è la più pulita: utilizza il property() integrato in modo semplice e diretto. Chiunque abbia dimestichezza con Python riconoscerà questo modello e non si verificherà alcun evento voodoo specifico del dominio.

Se non ti piace che ogni proprietà ha bisogno di tre linee per definire, Ecco un altro modo straight-forward:

from operator import attrgetter 

class Article(object): 
    def __init__(self, name, available): 
     self._name = name 
     self.available = available 
    name = property(attrgetter("_name")) 

In generale, non mi piace la definizione delle funzioni specifiche del dominio a fare qualcosa che possa essere fatto abbastanza facilmente con strumenti standard. Leggere il codice è molto più semplice se non ci si deve abituare a tutte le cose specifiche del progetto.

+2

Il tuo ragionamento su * perché * la risposta di Chris è così buona mi ha davvero conquistato. Ho pensato che un trucco intelligente sulla falsariga di quello che Rodrigo avrebbe pubblicato sarebbe stato davvero accurato ma, in effetti, ho ottenuto alcune sopracciglia alzate per tanta magia in un unico posto. –

Problemi correlati