Proprietà di sola lettura Python


95

Non so quando l'attributo dovrebbe essere privato e se dovrei usare la proprietà.

Ho letto di recente che setter e getter non sono pitonici e dovrei usare il decoratore di proprietà. Va bene.

Ma cosa succede se ho un attributo, che non deve essere impostato dall'esterno della classe ma può essere letto (attributo di sola lettura). Questo attributo dovrebbe essere privato e per privato intendo con trattino basso, in questo modo self._x? Se sì allora come posso leggerlo senza usare getter? L'unico metodo che conosco in questo momento è scrivere

@property
def x(self):
    return self._x

In questo modo posso leggere l'attributo di obj.xma non posso impostarlo, obj.x = 1quindi va bene.

Ma dovrei davvero preoccuparmi di impostare oggetti che non devono essere impostati? Forse dovrei semplicemente lasciarlo. Ma poi di nuovo non posso usare il carattere di sottolineatura perché la lettura obj._xè strana per l'utente, quindi dovrei usare obj.xe poi di nuovo l'utente non sa che non deve impostare questo attributo.

Qual è la tua opinione e le tue pratiche?


1
L'idea di una proprietà è che si comporta come un attributo ma può avere codice extra. Se tutto quello che vuoi è ottenere un valore, non mi preoccuperei nemmeno: usa self.xe confida che nessuno cambierà x. Se xè importante assicurarsi che ciò non possa essere modificato, utilizzare una proprietà.
li.davidm

Inoltre, _xnon è affatto strano: per convenzione , significa qualcosa di "privato".
li.davidm

1
Volevo dire che leggere da _x è strano. Non il nome _x stesso. Se l'utente sta leggendo direttamente da _x, non è responsabile.
Rafał Łużyński

3
Importante! La tua classe deve essere una classe di nuovo stile, cioè eredita da object, perché questo impedisca effettivamente l'impostazione obj.x. Su una classe vecchio stile puoi effettivamente ancora impostare obj.x, con risultati piuttosto inaspettati.
Ian H

Esistono diversi motivi validi per avere proprietà di sola lettura. Uno è quando hai un valore che consiste nell'unire altri due valori (lettura / scrittura). Puoi farlo in un metodo, ma puoi farlo anche in una proprietà di sola lettura.
philologon

Risposte:


68

In generale, i programmi Python dovrebbero essere scritti partendo dal presupposto che tutti gli utenti siano adulti consenzienti e quindi siano responsabili dell'uso corretto delle cose. Tuttavia, nel raro caso in cui non ha senso che un attributo sia impostabile (come un valore derivato o un valore letto da un'origine dati statica), la proprietà solo getter è generalmente il modello preferito.


26
Sembra che la tua risposta sia contraddittoria con se stessa. Dici che gli utenti dovrebbero essere responsabili e usare le cose correttamente, poi dici che a volte non ha senso che un attributo sia impostabile e la proprietà getter è un modo preferito. Secondo me puoi o non puoi impostare l'attributo. L'unica domanda è se devo proteggere questo attr o lasciarlo. Non dovrebbero esserci risposte intermedie.
Rafał Łużyński

19
No, ho detto che se non puoi letteralmente impostare un valore, non ha senso avere un setter. Ad esempio, se si dispone di un oggetto cerchio con un membro raggio e un attributo circonferenza derivato dal raggio, oppure se si dispone di un oggetto che racchiude alcune API in tempo reale di sola lettura con un numero di proprietà di sola getter. Niente che contraddica nulla.
Silas Ray

9
Ma l'utente responsabile non tenterà di impostare attr che letteralmente non può essere impostato. E ancora una volta l'utente non responsabile imposterebbe attr che letteralmente può essere impostato e solleverà errori da qualche altra parte nel codice a causa del suo set. Quindi alla fine non è possibile impostare entrambi gli attr. Dovrei usare la proprietà su entrambi o non usarla su nessuno?
Rafał Łużyński

8
Ma l'utente responsabile non dovrebbe provare a impostare attr che letteralmente non può essere impostato. Nella programmazione, se qualcosa è strettamente un valore non impostabile, la cosa responsabile o sensata è assicurarsi che non possa esserlo. Tutte queste piccole cose contribuiscono a programmi affidabili.
Robin Smith

6
Questa è una posizione che molte persone e lingue prendono. Se è una posizione che trovi non negoziabile, probabilmente non dovresti usare Python.
Silas Ray

72

Solo i miei due centesimi, Silas Ray è sulla strada giusta, tuttavia ho voluto aggiungere un esempio. ;-)

Python è un linguaggio non sicuro per i tipi e quindi dovrai sempre fidarti degli utenti del tuo codice per utilizzare il codice come una persona ragionevole (sensibile).

Per PEP 8 :

Utilizza un trattino basso iniziale solo per metodi non pubblici e variabili di istanza.

Per avere una proprietà 'di sola lettura' in una classe puoi usare la @propertydecorazione, dovrai ereditare da objectquando lo fai per usare le classi del nuovo stile.

Esempio:

>>> class A(object):
...     def __init__(self, a):
...         self._a = a
...
...     @property
...     def a(self):
...         return self._a
... 
>>> a = A('test')
>>> a.a
'test'
>>> a.a = 'pleh'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

9
Python non è un tipo pericoloso, è tipizzato dinamicamente. E il cambio di nome non serve per rendere più difficile imbrogliare, ma per prevenire conflitti di nomi in scenari in cui l'ereditarietà potrebbe essere problematica (se non stai programmando in grande non dovresti nemmeno preoccuparti).
memeplex

3
Ma dovresti comunque ricordare che gli oggetti modificabili possono essere modificati comunque usando questo metodo. Ad esempio self.__a = [], se puoi ancora farlo a.a.append('anything')e funzionerà.
Igor,

3
Non mi è chiaro che rapporto abbia "una persona ragionevole (sensibile)" su questa risposta. Puoi essere più esplicito riguardo al tipo di cose che pensi che una persona ragionevole farebbe e non farebbe?
winni2k

3
Per me, usa la decorazione @property, dovrai ereditare da object quando lo fai era il punto centrale di questa risposta. Grazie.
akki

2
@kkm l'unico modo per non consentire mai a un bug di intrufolarsi nel codice è non scrivere mai codice.
Alechan

55

Ecco un modo per evitare l'ipotesi che

tutti gli utenti sono adulti consenzienti e quindi sono responsabili dell'uso corretto delle cose.

si prega di consultare il mio aggiornamento di seguito

L'utilizzo @propertyè molto prolisso, ad esempio:

   class AClassWithManyAttributes:
        '''refactored to properties'''
        def __init__(a, b, c, d, e ...)
             self._a = a
             self._b = b
             self._c = c
             self.d = d
             self.e = e

        @property
        def a(self):
            return self._a
        @property
        def b(self):
            return self._b
        @property
        def c(self):
            return self._c
        # you get this ... it's long

Utilizzando

Nessun trattino basso: è una variabile pubblica.
Un carattere di sottolineatura: è una variabile protetta.
Due trattini bassi: è una variabile privata.

Tranne l'ultimo, è una convenzione. Puoi ancora, se ci provi davvero, accedere alle variabili con doppio trattino basso.

Quindi cosa facciamo? Rinunciamo a leggere solo le proprietà in Python?

Guarda! read_only_propertiesdecoratore in soccorso!

@read_only_properties('readonly', 'forbidden')
class MyClass(object):
    def __init__(self, a, b, c):
        self.readonly = a
        self.forbidden = b
        self.ok = c

m = MyClass(1, 2, 3)
m.ok = 4
# we can re-assign a value to m.ok
# read only access to m.readonly is OK 
print(m.ok, m.readonly) 
print("This worked...")
# this will explode, and raise AttributeError
m.forbidden = 4

Tu chiedi:

Da dove viene read_only_properties?

Sono contento che tu l'abbia chiesto, ecco la fonte per read_only_properties :

def read_only_properties(*attrs):

    def class_rebuilder(cls):
        "The class decorator"

        class NewClass(cls):
            "This is the overwritten class"
            def __setattr__(self, name, value):
                if name not in attrs:
                    pass
                elif name not in self.__dict__:
                    pass
                else:
                    raise AttributeError("Can't modify {}".format(name))

                super().__setattr__(name, value)
        return NewClass
    return class_rebuilder

aggiornare

Non mi sarei mai aspettato che questa risposta ricevesse così tanta attenzione. Sorprendentemente lo fa. Questo mi ha incoraggiato a creare un pacchetto che puoi usare.

$ pip install read-only-properties

nella tua shell python:

In [1]: from rop import read_only_properties

In [2]: @read_only_properties('a')
   ...: class Foo:
   ...:     def __init__(self, a, b):
   ...:         self.a = a
   ...:         self.b = b
   ...:         

In [3]: f=Foo('explodes', 'ok-to-overwrite')

In [4]: f.b = 5

In [5]: f.a = 'boom'
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-5-a5226072b3b4> in <module>()
----> 1 f.a = 'boom'

/home/oznt/.virtualenvs/tracker/lib/python3.5/site-packages/rop.py in __setattr__(self, name, value)
    116                     pass
    117                 else:
--> 118                     raise AttributeError("Can't touch {}".format(name))
    119 
    120                 super().__setattr__(name, value)

AttributeError: Can't touch a

1
Questo è davvero utile e fa esattamente quello che volevo fare. Grazie. Tuttavia, è per coloro che hanno installato Python 3. Sto usando Python 2.7.8, quindi devo applicare due piccole modifiche alla tua soluzione: "class NewClass (cls, <b> object <\ b>):" ... "<b> super (NewClass, self) <\ b> .__ setattr __ (nome, valore) ".
Ying Zhang

1
Inoltre, si dovrebbe fare attenzione che le variabili dei membri della classe siano elenchi e dizionari. Non puoi davvero "bloccarli" dall'aggiornamento in questo modo.
Ying Zhang

1
Un miglioramento e tre problemi qui. Miglioramento: il if..elif..elseblocco potrebbe essere semplicemente if name in attrs and name in self.__dict__: raise Attr...senza passnecessità. Problema 1: le classi così decorate finiscono tutte con un identico __name__, e anche la rappresentazione di stringa del loro tipo è omogeneizzata. Problema 2: questa decorazione sovrascrive qualsiasi costume __setattr__. Problema 3: gli utenti possono sconfiggerlo con del MyClass.__setattr__.
TigerhawkT3

Solo una cosa linguistica. "Ahimè ..." significa "Triste a dirsi, ..." che non è quello che vuoi, penso.
Thomas Andrews

Niente mi impedirà di fare object.__setattr__(f, 'forbidden', 42). Non vedo cosa read_only_propertiesaggiunge che non sia gestito dal doppio carattere di sottolineatura che altera il nome.
L3viathan

4

Ecco un approccio leggermente diverso alle proprietà di sola lettura, che forse dovrebbero essere chiamate proprietà write-once poiché devono essere inizializzate, no? Per i paranoici tra noi che si preoccupano di poter modificare le proprietà accedendo direttamente al dizionario dell'oggetto, ho introdotto la manipolazione dei nomi "estrema":

from uuid import uuid4

class Read_Only_Property:
    def __init__(self, name):
        self.name = name
        self.dict_name = uuid4().hex
        self.initialized = False

    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.dict_name]

    def __set__(self, instance, value):
        if self.initialized:
            raise AttributeError("Attempt to modify read-only property '%s'." % self.name)
        instance.__dict__[self.dict_name] = value
        self.initialized = True

class Point:
    x = Read_Only_Property('x')
    y = Read_Only_Property('y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

if __name__ == '__main__':
    try:
        p = Point(2, 3)
        print(p.x, p.y)
        p.x = 9
    except Exception as e:
        print(e)

Bello. Se dict_nameinvece maneggi , ad esempio dict_name = "_spam_" + name, rimuove la dipendenza da uuid4e rende il debug molto più semplice.
cz

Ma poi posso dire p.__dict__['_spam_x'] = 5di cambiare il valore di p.x, quindi questo non fornisce sufficiente alterazione del nome.
Booboo

1

Non sono soddisfatto delle due risposte precedenti per creare proprietà di sola lettura perché la prima soluzione consente di eliminare e quindi impostare l'attributo readonly e non blocca __dict__. La seconda soluzione potrebbe essere aggirata con il test: trovare il valore che è uguale a quello che hai impostato due e modificarlo alla fine.

Ora, per il codice.

def final(cls):
    clss = cls
    @classmethod
    def __init_subclass__(cls, **kwargs):
        raise TypeError("type '{}' is not an acceptable base type".format(clss.__name__))
    cls.__init_subclass__ = __init_subclass__
    return cls


def methoddefiner(cls, method_name):
    for clss in cls.mro():
        try:
            getattr(clss, method_name)
            return clss
        except(AttributeError):
            pass
    return None


def readonlyattributes(*attrs):
    """Method to create readonly attributes in a class

    Use as a decorator for a class. This function takes in unlimited 
    string arguments for names of readonly attributes and returns a
    function to make the readonly attributes readonly. 

    The original class's __getattribute__, __setattr__, and __delattr__ methods
    are redefined so avoid defining those methods in the decorated class

    You may create setters and deleters for readonly attributes, however
    if they are overwritten by the subclass, they lose access to the readonly
    attributes. 

    Any method which sets or deletes a readonly attribute within
    the class loses access if overwritten by the subclass besides the __new__
    or __init__ constructors.

    This decorator doesn't support subclassing of these classes
    """
    def classrebuilder(cls):
        def __getattribute__(self, name):
            if name == '__dict__':
                    from types import MappingProxyType
                    return MappingProxyType(super(cls, self).__getattribute__('__dict__'))
            return super(cls, self).__getattribute__(name)
        def __setattr__(self, name, value): 
                if name == '__dict__' or name in attrs:
                    import inspect
                    stack = inspect.stack()
                    try:
                        the_class = stack[1][0].f_locals['self'].__class__
                    except(KeyError):
                        the_class = None
                    the_method = stack[1][0].f_code.co_name
                    if the_class != cls: 
                         if methoddefiner(type(self), the_method) != cls:
                            raise AttributeError("Cannot set readonly attribute '{}'".format(name))                        
                return super(cls, self).__setattr__(name, value)
        def __delattr__(self, name):                
                if name == '__dict__' or name in attrs:
                    import inspect
                    stack = inspect.stack()
                    try:
                        the_class = stack[1][0].f_locals['self'].__class__
                    except(KeyError):
                        the_class = None
                    the_method = stack[1][0].f_code.co_name
                    if the_class != cls:
                        if methoddefiner(type(self), the_method) != cls:
                            raise AttributeError("Cannot delete readonly attribute '{}'".format(name))                        
                return super(cls, self).__delattr__(name)
        clss = cls
        cls.__getattribute__ = __getattribute__
        cls.__setattr__ = __setattr__
        cls.__delattr__ = __delattr__
        #This line will be moved when this algorithm will be compatible with inheritance
        cls = final(cls)
        return cls
    return classrebuilder

def setreadonlyattributes(cls, *readonlyattrs):
    return readonlyattributes(*readonlyattrs)(cls)


if __name__ == '__main__':
    #test readonlyattributes only as an indpendent module
    @readonlyattributes('readonlyfield')
    class ReadonlyFieldClass(object):
        def __init__(self, a, b):
            #Prevent initalization of the internal, unmodified PrivateFieldClass
            #External PrivateFieldClass can be initalized
            self.readonlyfield = a
            self.publicfield = b


    attr = None
    def main():
        global attr
        pfi = ReadonlyFieldClass('forbidden', 'changable')
        ###---test publicfield, ensure its mutable---###
        try:
            #get publicfield
            print(pfi.publicfield)
            print('__getattribute__ works')
            #set publicfield
            pfi.publicfield = 'mutable'
            print('__setattr__ seems to work')
            #get previously set publicfield
            print(pfi.publicfield)
            print('__setattr__ definitely works')
            #delete publicfield
            del pfi.publicfield 
            print('__delattr__ seems to work')
            #get publicfield which was supposed to be deleted therefore should raise AttributeError
            print(pfi.publlicfield)
            #publicfield wasn't deleted, raise RuntimeError
            raise RuntimeError('__delattr__ doesn\'t work')
        except(AttributeError):
            print('__delattr__ works')


        try:
            ###---test readonly, make sure its readonly---###
            #get readonlyfield
            print(pfi.readonlyfield)
            print('__getattribute__ works')
            #set readonlyfield, should raise AttributeError
            pfi.readonlyfield = 'readonly'
            #apparently readonlyfield was set, notify user
            raise RuntimeError('__setattr__ doesn\'t work')
        except(AttributeError):
            print('__setattr__ seems to work')
            try:
                #ensure readonlyfield wasn't set
                print(pfi.readonlyfield)
                print('__setattr__ works')
                #delete readonlyfield
                del pfi.readonlyfield
                #readonlyfield was deleted, raise RuntimeError
                raise RuntimeError('__delattr__ doesn\'t work')
            except(AttributeError):
                print('__delattr__ works')
        try:
            print("Dict testing")
            print(pfi.__dict__, type(pfi.__dict__))
            attr = pfi.readonlyfield
            print(attr)
            print("__getattribute__ works")
            if pfi.readonlyfield != 'forbidden':
                print(pfi.readonlyfield)
                raise RuntimeError("__getattr__ doesn't work")
            try:
                pfi.__dict__ = {}
                raise RuntimeError("__setattr__ doesn't work")
            except(AttributeError):
                print("__setattr__ works")
            del pfi.__dict__
            raise RuntimeError("__delattr__ doesn't work")
        except(AttributeError):
            print(pfi.__dict__)
            print("__delattr__ works")
            print("Basic things work")


main()

Non ha senso creare attributi di sola lettura tranne quando si scrive il codice della libreria, codice che viene distribuito ad altri come codice da utilizzare per migliorare i propri programmi, non codice per altri scopi, come lo sviluppo di app. Il problema __dict__ è stato risolto, perché __dict__ è ora dei tipi immutabili.MappingProxyType , quindi gli attributi non possono essere modificati tramite __dict__. Anche l'impostazione o l'eliminazione di __dict__ è bloccata. L'unico modo per modificare le proprietà di sola lettura è cambiare i metodi della classe stessa.

Anche se credo che la mia soluzione sia migliore delle due precedenti, potrebbe essere migliorata. Questi sono i punti deboli di questo codice:

a) Non consente l'aggiunta a un metodo in una sottoclasse che imposta o elimina un attributo di sola lettura. Un metodo definito in una sottoclasse viene automaticamente escluso dall'accesso a un attributo di sola lettura, anche chiamando la versione del metodo della superclasse.

b) I metodi di sola lettura della classe possono essere modificati per annullare le restrizioni di sola lettura.

Tuttavia, non c'è modo senza modificare la classe per impostare o eliminare un attributo di sola lettura. Questo non dipende dalle convenzioni di denominazione, il che è positivo perché Python non è così coerente con le convenzioni di denominazione. Ciò fornisce un modo per creare attributi di sola lettura che non possono essere modificati con scappatoie nascoste senza modificare la classe stessa. Elenca semplicemente gli attributi da leggere solo quando chiami il decoratore come argomenti e diventeranno di sola lettura.

Ringraziamo la risposta di Brice in Come ottenere il nome della classe chiamante all'interno di una funzione di un'altra classe in Python? per ottenere le classi e i metodi del chiamante.


object.__setattr__(pfi, 'readonly', 'foobar')rompe questa soluzione, senza modificare la classe stessa.
L3viathan

0

Si noti che i metodi di istanza sono anche attributi (della classe) e che è possibile impostarli a livello di classe o di istanza se si vuole davvero essere un tosto. O che tu possa impostare una variabile di classe (che è anche un attributo della classe), dove le utili proprietà di sola lettura non funzioneranno perfettamente fuori dalla scatola. Quello che sto cercando di dire è che il problema dell '"attributo di sola lettura" è in effetti più generale di quanto si percepisca normalmente. Fortunatamente ci sono aspettative convenzionali al lavoro che sono così forti da accecarci rispetto a questi altri casi (dopo tutto, quasi tutto è un attributo di qualche tipo in Python).

Basandosi su queste aspettative, penso che l'approccio più generale e leggero sia quello di adottare la convenzione che gli attributi "pubblici" (nessun trattino di sottolineatura iniziale) sono di sola lettura tranne quando esplicitamente documentati come scrivibili. Questo sussume la solita aspettativa che i metodi non vengano patchati e le variabili di classe che indicano i valori predefiniti dell'istanza sono meglio per non parlare. Se ti senti davvero paranoico su qualche attributo speciale, usa un descrittore di sola lettura come ultima misura di risorsa.


0

Sebbene mi piaccia il decoratore di classi di Oz123, potresti anche fare quanto segue, che utilizza un wrapper di classe esplicito e __new__ con un metodo class Factory che restituisce la classe all'interno di una chiusura:

class B(object):
    def __new__(cls, val):
        return cls.factory(val)

@classmethod
def factory(cls, val):
    private = {'var': 'test'}

    class InnerB(object):
        def __init__(self):
            self.variable = val
            pass

        @property
        def var(self):
            return private['var']

    return InnerB()

dovresti aggiungere alcuni test che mostrano come funziona con più proprietà
Oz123

0

Questa è la mia soluzione alternativa.

@property
def language(self):
    return self._language
@language.setter
def language(self, value):
    # WORKAROUND to get a "getter-only" behavior
    # set the value only if the attribute does not exist
    try:
        if self.language == value:
            pass
        print("WARNING: Cannot set attribute \'language\'.")
    except AttributeError:
        self._language = value

0

qualcuno ha menzionato l'uso di un oggetto proxy, non ne ho visto un esempio, quindi ho finito per provarlo, [male].

/! \ Se possibile, preferisci definizioni di classe e costruttori di classi

questo codice viene effettivamente riscritto class.__new__(costruttore di classi) tranne che peggio in ogni modo. Risparmia il dolore e non usare questo schema se puoi.

def attr_proxy(obj):
    """ Use dynamic class definition to bind obj and proxy_attrs.
        If you can extend the target class constructor that is 
        cleaner, but its not always trivial to do so.
    """
    proxy_attrs = dict()

    class MyObjAttrProxy():
        def __getattr__(self, name):
            if name in proxy_attrs:
                return proxy_attrs[name]  # overloaded

            return getattr(obj, name)  # proxy

        def __setattr__(self, name, value):
            """ note, self is not bound when overloading methods
            """
            proxy_attrs[name] = value

    return MyObjAttrProxy()


myobj = attr_proxy(Object())
setattr(myobj, 'foo_str', 'foo')

def func_bind_obj_as_self(func, self):
    def _method(*args, **kwargs):
        return func(self, *args, **kwargs)
    return _method

def mymethod(self, foo_ct):
    """ self is not bound because we aren't using object __new__
        you can write the __setattr__ method to bind a self 
        argument, or declare your functions dynamically to bind in 
        a static object reference.
    """
    return self.foo_str + foo_ct

setattr(myobj, 'foo', func_bind_obj_as_self(mymethod, myobj))

-2

So che sto riportando in vita questo thread, ma stavo cercando di rendere una proprietà di sola lettura e dopo aver trovato questo argomento, non ero soddisfatto delle soluzioni già condivise.

Quindi, tornando alla domanda iniziale, se inizi con questo codice:

@property
def x(self):
    return self._x

E vuoi rendere X di sola lettura, puoi semplicemente aggiungere:

@x.setter
def x(self, value):
    raise Exception("Member readonly")

Quindi, se esegui quanto segue:

print (x) # Will print whatever X value is
x = 3 # Will raise exception "Member readonly"

3
Ma se semplicemente non fai un setter, provare ad assegnare genererà anche un errore (An AttributeError('can't set attribute'))
Artyer
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.