Propriedade somente leitura do Python

95

Não sei quando o atributo deve ser privado e se devo usar a propriedade.

Eu li recentemente que setters e getters não são pythônicos e eu deveria usar o decorador de propriedade. Está certo.

Mas e se eu tiver um atributo, que não deve ser definido de fora da classe, mas pode ser lido (atributo somente leitura). Este atributo deve ser privado, e por privado quero dizer com sublinhado, assim self._x? Se sim, como posso ler sem usar getter? O único método que conheço agora é escrever

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

Dessa forma, posso ler o atributo, obj.xmas não consigo defini-lo, obj.x = 1então está tudo bem.

Mas devo realmente me preocupar em definir o objeto que não deve ser definido? Talvez eu deva apenas deixar isso. Mas, novamente, não posso usar o sublinhado porque a leitura obj._xé estranha para o usuário, então devo usar obj.xe, novamente, o usuário não sabe que não deve definir este atributo.

Qual é a sua opinião e prática?

Rafał Łużyński
fonte
1
A ideia de uma propriedade é que ela se comporte como um atributo, mas pode ter um código extra. Se tudo que você quer é obter um valor, eu nem me incomodaria: basta usar self.xe confiar que ninguém vai mudar x. Se certificar-se de que isso xnão pode ser alterado for importante, use uma propriedade.
li.davidm
Além disso, _xnão é nada estranho: por convenção , significa algo "privado".
li.davidm
1
Eu quis dizer que ler _x é estranho. Não o próprio nome _x. Se o usuário estiver lendo diretamente de _x, ele não será responsável.
Rafał Łużyński
3
Importante! Sua classe deve ser uma classe de novo estilo, ou seja, herda de object, para que isso realmente pare de configurar obj.x. Em uma classe de estilo antigo, você ainda pode definir obj.x, com resultados bastante inesperados.
Ian H
Existem vários motivos válidos para ter propriedades somente leitura. Um é quando você tem um valor que consiste em mesclar dois outros valores (leitura / gravação). Você pode fazer isso em um método, mas também em uma propriedade somente leitura.
philologon

Respostas:

68

Geralmente, os programas Python devem ser escritos com a suposição de que todos os usuários são adultos consentidos e, portanto, são responsáveis ​​por usar as coisas corretamente. No entanto, na rara instância em que simplesmente não faz sentido que um atributo seja configurável (como um valor derivado ou um valor lido de alguma fonte de dados estática), a propriedade somente getter é geralmente o padrão preferido.

Silas Ray
fonte
26
Parece que sua resposta é contraditória. Você diz que os usuários devem ser responsáveis ​​e usar as coisas corretamente, então você diz que às vezes não faz sentido que um atributo seja configurável e que a propriedade getter é a forma preferida. Na minha opinião, você pode ou não definir atributos. A única questão é se devo proteger ou deixá-lo. Não deve haver nenhuma resposta no meio.
Rafał Łużyński
19
Não, eu disse que se você literalmente não pode definir um valor, então não faz sentido ter um setter. Por exemplo, se você tiver um objeto de círculo com um membro de raio e um atributo de circunferência que é derivado do raio, ou você tem um objeto que envolve alguma api de tempo real somente leitura com várias propriedades somente getter. Nada contradizendo nada.
Silas Ray
9
Mas o usuário responsável não tentará definir atributos que literalmente não podem ser definidos. E, novamente, o usuário não responsável definiria attr que literalmente pode ser definido e gerará erro em outro lugar no código devido ao seu conjunto. Portanto, no final, ambos os atributos não podem ser definidos. Devo usar a propriedade em ambos ou não em nenhuma?
Rafał Łużyński
8
Mas o usuário responsável não deve tentar definir atributos que literalmente não podem ser definidos. Na programação, se algo é um valor estritamente indefinível, o responsável ou sensato é garantir que não o seja. Todas essas pequenas coisas contribuem para programas confiáveis.
Robin Smith de
6
Essa é uma posição que muitas pessoas e idiomas assumem. Se for uma posição que você achar inegociável, provavelmente não deveria usar Python.
Silas Ray
72

Apenas meus dois centavos, Silas Ray está no caminho certo, no entanto, queria adicionar um exemplo. ;-)

Python é uma linguagem insegura e, portanto, você sempre terá que confiar que os usuários de seu código usarão o código como uma pessoa razoável (sensata).

Por PEP 8 :

Use um sublinhado inicial apenas para métodos não públicos e variáveis ​​de instância.

Para ter uma propriedade 'somente leitura' em uma classe de que você pode fazer uso da @propertydecoração, você precisará herdar de objectquando fizer isso para usar as classes de novo estilo.

Exemplo:

>>> 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
siebz0r
fonte
9
Python não tem tipo inseguro, é digitado dinamicamente. E a mutilação de nomes não é para tornar mais difícil trapacear, mas para evitar conflitos de nomes em cenários onde a herança pode ser problemática (se você não estiver programando no grande, nem se importe).
memeplex de
3
Mas você ainda deve se lembrar, que objetos mutáveis ​​podem ser alterados usando este método de qualquer maneira. Por exemplo self.__a = [], se , você ainda pode fazer isso a.a.append('anything')e vai funcionar.
Igor
3
Não está claro para mim o que significa "uma pessoa razoável (sensata)" para essa resposta. Você pode ser mais explícito sobre os tipos de coisas que acha que uma pessoa razoável faria e não faria?
winni2k de
3
Para que eu use a decoração @property, você precisará herdar de objeto ao fazer isso, foi o ponto principal para esta resposta. Obrigado.
akki de
2
@kkm a única maneira de nunca permitir que um bug entre no código é nunca escrever código.
Alechan
55

Aqui está uma maneira de evitar a suposição de que

todos os usuários são adultos consentidos e, portanto, são responsáveis ​​por usar as coisas corretamente.

por favor veja minha atualização abaixo

Usando @property, é muito detalhado, por exemplo:

   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

Usando

Sem sublinhado: é uma variável pública.
Um sublinhado: é uma variável protegida.
Dois sublinhados: é uma variável privada.

Exceto o último, é uma convenção. Você ainda pode, se realmente tentar, acessar variáveis ​​com sublinhado duplo.

Então, o que fazemos? Desistimos de ter propriedades somente leitura em Python?

Ver! read_only_propertiesdecorador para o resgate!

@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

Você pergunta:

De onde read_only_propertiesvem?

Que bom que você perguntou, aqui está a fonte para 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

atualizar

Nunca esperei que essa resposta recebesse tanta atenção. Surpreendentemente, sim. Isso me incentivou a criar um pacote que você possa usar.

$ pip install read-only-properties

em seu 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
Oz123
fonte
1
Isso é muito útil e faz exatamente o que eu queria fazer. Obrigado. No entanto, é para aqueles que têm o Python 3 instalado. Estou usando o Python 2.7.8, então tenho que aplicar dois pequenos ajustes à sua solução: "class NewClass (cls, <b> objeto <\ b>):" ... "<b> super (NewClass, self) <\ b> .__ setattr __ (nome, valor) ".
Ying Zhang
1
Além disso, deve-se ter cuidado com o fato de as variáveis ​​de membros da classe serem listas e dicionários. Você não pode realmente 'impedi-los' de serem atualizados dessa maneira.
Ying Zhang
1
Uma melhoria e três problemas aqui. Melhoria: o if..elif..elsebloqueio poderia ser apenas if name in attrs and name in self.__dict__: raise Attr...sem necessidade pass. Problema 1: as classes assim decoradas terminam todas com um idêntico __name__, e a representação em string de seu tipo também é homogeneizada. Problema 2: esta decoração sobrescreve qualquer custom __setattr__. Problema 3: os usuários podem derrotar isso com del MyClass.__setattr__.
TigerhawkT3
Apenas uma coisa de linguagem. "Infelizmente ..." significa "É triste dizer, ..." que não é o que você quer, eu acho.
Thomas Andrews
Nada me impedirá de fazer object.__setattr__(f, 'forbidden', 42). Não vejo o que read_only_propertiesadiciona que não seja manipulado pela mutilação do nome de sublinhado duplo.
L3viathan
4

Aqui está uma abordagem um pouco diferente para propriedades somente leitura, que talvez devam ser chamadas de propriedades de gravação única, uma vez que precisam ser inicializadas, não é? Para os paranóicos entre nós que se preocupam em poder modificar propriedades acessando o dicionário do objeto diretamente, introduzi a mutilação "extrema" de nomes:

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)
Booboo
fonte
Agradável. Se você alterar dict_name, por exemplo dict_name = "_spam_" + name, remove a dependência uuid4e torna a depuração muito mais fácil.
cz
Mas então posso dizer p.__dict__['_spam_x'] = 5para alterar o valor de p.x, portanto, isso não fornece mutilação de nome suficiente.
Booboo,
1

Estou insatisfeito com as duas respostas anteriores para criar propriedades somente leitura porque a primeira solução permite que o atributo somente leitura seja excluído e, em seguida, definido e não bloqueia o __dict__. A segunda solução pode ser contornada com testes - encontrar o valor igual ao que você definiu como dois e alterá-lo eventualmente.

Agora, para o código.

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

Não faz sentido criar atributos somente leitura, exceto quando está escrevendo código de biblioteca, código que está sendo distribuído a outros como código para usar a fim de aprimorar seus programas, e não código para qualquer outro propósito, como desenvolvimento de aplicativo. O problema __dict__ foi resolvido, porque o __dict__ agora é dos tipos imutáveis.MappingProxyType , portanto, os atributos não podem ser alterados por meio do __dict__. Definir ou excluir __dict__ também é bloqueado. A única maneira de alterar as propriedades somente leitura é alterando os métodos da própria classe.

Embora eu acredite que minha solução seja melhor do que as duas anteriores, ela poderia ser melhorada. Estes são os pontos fracos deste código:

a) Não permite adicionar a um método em uma subclasse que define ou exclui um atributo somente leitura. Um método definido em uma subclasse é automaticamente impedido de acessar um atributo somente leitura, mesmo chamando a versão da superclasse do método.

b) Os métodos somente leitura da classe podem ser alterados para evitar as restrições de somente leitura.

No entanto, não há como, sem editar a classe, definir ou excluir um atributo somente leitura. Isso não depende de convenções de nomenclatura, o que é bom porque o Python não é tão consistente com as convenções de nomenclatura. Isso fornece uma maneira de criar atributos somente leitura que não podem ser alterados com lacunas ocultas sem editar a própria classe. Simplesmente liste os atributos a serem lidos somente ao chamar o decorador como argumentos e eles se tornarão somente leitura.

Crédito para a resposta de Brice em Como obter o nome da classe do chamador dentro de uma função de outra classe em python? para obter as classes e métodos do chamador.

Michael
fonte
object.__setattr__(pfi, 'readonly', 'foobar')quebra essa solução, sem editar a própria classe.
L3viathan
0

Observe que os métodos de instância também são atributos (da classe) e que você pode defini-los no nível da classe ou da instância se realmente quiser ser durão. Ou que você pode definir uma variável de classe (que também é um atributo da classe), onde as propriedades úteis somente leitura não funcionarão perfeitamente fora da caixa. O que estou tentando dizer é que o problema do "atributo somente leitura" é, na verdade, mais geral do que normalmente é percebido. Felizmente, existem expectativas convencionais em ação que são tão fortes que nos cegam contra esses outros casos (afinal, quase tudo é um atributo de algum tipo em python).

Com base nessas expectativas, acho que a abordagem mais geral e leve é ​​adotar a convenção de que os atributos "públicos" (sem sublinhado inicial) são somente leitura, exceto quando explicitamente documentados como graváveis. Isso inclui a expectativa usual de que os métodos não serão corrigidos e as variáveis ​​de classe que indicam os padrões da instância são melhores, quanto mais. Se você se sentir realmente paranóico com algum atributo especial, use um descritor somente leitura como medida de último recurso.

memeplex
fonte
0

Embora eu goste do decorador de classe de Oz123, você também pode fazer o seguinte, que usa um wrapper de classe explícito e __new__ com um método Factory de classe que retorna a classe dentro de um encerramento:

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()
Apollo Marquis
fonte
você deve adicionar algum teste mostrando como funciona com várias propriedades
Oz123
0

Essa é a minha solução 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
rusiano
fonte
0

alguém mencionou usar um objeto proxy, eu não vi um exemplo disso, então acabei experimentando, [mal].

/! \ Por favor, prefira definições de classe e construtores de classe, se possível

este código está reescrevendo efetivamente class.__new__(construtor de classe), exceto pior em todos os sentidos. Evite a dor e não use esse padrão, se puder.

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 revs
fonte
-2

Eu sei que estou trazendo de volta dos mortos este tópico, mas eu estava procurando como tornar uma propriedade somente leitura e depois de encontrar este tópico, não fiquei satisfeito com as soluções já compartilhadas.

Então, voltando à pergunta inicial, se você começar com este código:

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

E você deseja tornar o X somente leitura, você pode simplesmente adicionar:

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

Então, se você executar o seguinte:

print (x) # Will print whatever X value is
x = 3 # Will raise exception "Member readonly"
vincedjango
fonte
3
Mas se você simplesmente não fizer um setter, tentar atribuir gerará um erro também (An AttributeError('can't set attribute'))
Artyer