Propiedad de solo lectura de Python

95

No sé cuándo el atributo debería ser privado y si debería usar la propiedad.

Recientemente leí que los setters y getters no son pitónicos y debería usar el decorador de propiedades. Está bien.

Pero, ¿qué pasa si tengo un atributo, que no se debe configurar desde fuera de la clase, pero se puede leer (atributo de solo lectura). ¿Este atributo debería ser privado, y por privado me refiero con guión bajo, así self._x? Si es así, ¿cómo puedo leerlo sin usar getter? El único método que conozco ahora es escribir

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

De esa manera puedo leer el atributo obj.xpero no puedo configurarlo, obj.x = 1así que está bien.

Pero, ¿debería realmente preocuparme por configurar un objeto que no debe configurarse? Quizás debería dejarlo. Pero, de nuevo, no puedo usar guión bajo porque la lectura obj._xes extraña para el usuario, por lo que debería usar obj.xy, nuevamente, el usuario no sabe que no debe establecer este atributo.

¿Cuál es tu opinión y tus prácticas?

Rafał Łużyński
fuente
1
La idea de una propiedad es que se comporta como un atributo pero puede tener código adicional. Si todo lo que quieres es obtener un valor, ni siquiera me molestaría: solo usa self.xy confía en que nadie cambiará x. Si xes importante asegurarse de que no se pueda cambiar, utilice una propiedad.
li.davidm
Además, _xno es extraño en absoluto: por convención , significa algo "privado".
li.davidm
1
Quise decir que leer de _x es extraño. No el nombre _x en sí. Si el usuario está leyendo directamente desde _x, entonces no es responsable.
Rafał Łużyński
3
¡Importante! Su clase debe ser una clase de estilo nuevo, es decir, heredada object, para que esto realmente detenga su configuración obj.x. En una clase de estilo antiguo, aún puede configurar obj.x, con resultados bastante inesperados.
Ian H
Hay varias razones válidas para tener propiedades de solo lectura. Una es cuando tiene un valor que consiste en fusionar otros dos valores (lectura / escritura). Puede hacer esto en un método, pero también puede hacerlo en una propiedad de solo lectura.
philologon

Respuestas:

68

Generalmente, los programas de Python deben escribirse asumiendo que todos los usuarios son adultos y, por lo tanto, son responsables de usar las cosas correctamente. Sin embargo, en el raro caso en el que simplemente no tiene sentido que un atributo sea configurable (como un valor derivado o un valor leído de alguna fuente de datos estática), la propiedad de solo captador es generalmente el patrón preferido.

Silas Ray
fuente
26
Parece que tu respuesta se contradice a sí misma. Usted dice que los usuarios deben ser responsables y usar las cosas correctamente, luego dice que a veces no tiene sentido que un atributo sea configurable y que la propiedad getter sea la forma preferida. En mi opinión, puede o no puede establecer un atributo. La única pregunta es si debería proteger este atributo o dejarlo. No debería haber respuestas intermedias.
Rafał Łużyński
19
No, dije que si literalmente no puedes establecer un valor, entonces no tiene sentido tener un establecedor. Por ejemplo, si tiene un objeto de círculo con un miembro de radio y un atributo de circunferencia que se deriva del radio, o tiene un objeto que envuelve una API en tiempo real de solo lectura con varias propiedades de solo captador. Nada que contradiga nada.
Silas Ray
9
Pero el usuario responsable no intentará establecer un atributo que literalmente no se pueda establecer. Y de nuevo, el usuario no responsable establecería un atributo que literalmente se puede configurar y generará un error en otro lugar del código debido a su configuración. Entonces, al final, no se pueden establecer ambos atributos. ¿Debo usar la propiedad en ambos o no usarla en ninguno?
Rafał Łużyński
8
Pero el usuario responsable no debe intentar establecer un atributo que literalmente no se puede establecer. En programación, si algo es estrictamente un valor no configurable, lo responsable o sensato es asegurar que no pueda serlo. Todas estas pequeñas cosas contribuyen a programas confiables.
Robin Smith
6
Esa es una posición que adoptan muchas personas e idiomas. Si es una posición que no encuentra negociable, probablemente no debería usar Python.
Silas Ray
72

Solo mis dos centavos, Silas Ray está en el camino correcto, sin embargo, tuve ganas de agregar un ejemplo. ;-)

Python es un lenguaje de tipo inseguro y, por lo tanto, siempre tendrá que confiar en los usuarios de su código para usar el código como una persona razonable (sensata).

Por PEP 8 :

Utilice un subrayado inicial solo para métodos no públicos y variables de instancia.

Para tener una propiedad de 'solo lectura' en una clase, puede hacer uso de la @propertydecoración, deberá heredar de objectcuando lo haga para hacer uso de las clases de nuevo estilo.

Ejemplo:

>>> 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
fuente
9
Python no es un tipo inseguro, se escribe dinámicamente. Y la alteración de nombres no es para dificultar las trampas, sino para evitar conflictos de nombres en escenarios donde la herencia podría ser problemática (si no está programando en grande, no debería importarle).
memeplex
3
Pero aún debe recordar que los objetos mutables se pueden cambiar usando este método de todos modos. Por ejemplo self.__a = [], si aún puede hacer esto a.a.append('anything')y funcionará.
Igor
3
No me queda claro qué relación tiene "una persona razonable (sensata)" con esta respuesta. ¿Puede ser más explícito sobre el tipo de cosas que cree que una persona razonable haría y no haría?
winni2k
3
Para mí, haga uso de la decoración de @property, necesitará heredar del objeto cuando lo haga, fue el objetivo de esta respuesta. Gracias.
Akki
2
@kkm la única forma de no permitir que un error se filtre en el código es nunca escribir código.
Alechan
55

Aquí hay una forma de evitar la suposición de que

todos los usuarios son adultos que consienten y, por lo tanto, son responsables de usar las cosas correctamente ellos mismos.

por favor mira mi actualización a continuación

El uso @propertyes muy detallado, por ejemplo:

   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

Utilizando

Sin subrayado: es una variable pública.
Un subrayado: es una variable protegida.
Dos guiones bajos: es una variable privada.

Excepto el último, es una convención. Aún puede, si realmente se esfuerza, acceder a las variables con doble subrayado.

¿Asi que que hacemos? ¿Renunciamos a tener propiedades de solo lectura en Python?

¡Mirad! read_only_propertiesdecorador al rescate!

@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

Usted pregunta:

De donde read_only_propertiesviene

Me alegro de que lo hayas preguntado, aquí está la fuente de 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

actualizar

Nunca esperé que esta respuesta recibiera tanta atención. Sorprendentemente lo hace. Esto me animó a crear un paquete que puedas usar.

$ pip install read-only-properties

en tu caparazón de 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
fuente
1
Esto es realmente útil y hace exactamente lo que quería hacer. Gracias. Sin embargo, es para aquellos que tienen Python 3 instalado. Estoy usando Python 2.7.8, así que tengo que aplicar dos ajustes menores a su solución: "clase NewClass (cls, <b> objeto <\ b>):" ... "<b> super (NewClass, self) <\ b> .__ setattr __ (nombre, valor) ".
Ying Zhang
1
Además, se debe tener cuidado con que las variables de los miembros de la clase sean listas y diccionarios. Realmente no puede 'bloquearlos' para que no se actualicen de esta manera.
Ying Zhang
1
Una mejora y tres problemas aquí. Mejora: el if..elif..elsebloque podría ser simplemente if name in attrs and name in self.__dict__: raise Attr...sin passnecesidad. Problema 1: las clases así decoradas terminan todas con una idéntica __name__, y la representación de cadena de su tipo también se homogeneiza. Problema 2: esta decoración sobrescribe cualquier costumbre __setattr__. Problema 3: los usuarios pueden vencer esto con del MyClass.__setattr__.
TigerhawkT3
Solo una cuestión de lenguaje. "Ay ..." significa "Es triste decirlo ..." que no es lo que quieres, creo.
Thomas Andrews
Nada me impedirá hacer object.__setattr__(f, 'forbidden', 42). No veo qué read_only_propertiesagrega que no sea manejado por la alteración del nombre de doble subrayado.
L3viatán
4

Aquí hay un enfoque ligeramente diferente para las propiedades de solo lectura, que tal vez deberían llamarse propiedades de una sola escritura ya que tienen que inicializarse, ¿no es así? Para los paranoicos entre nosotros que se preocupan por poder modificar propiedades accediendo directamente al diccionario del objeto, he introducido la alteración de nombres "extrema":

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)
Boo Boo
fuente
Agradable. Si, en dict_namecambio, modifica , por ejemplo dict_name = "_spam_" + name, elimina la dependencia de uuid4y hace que la depuración sea mucho más fácil.
cz
Pero luego puedo decir p.__dict__['_spam_x'] = 5que cambie el valor de p.x, por lo que esto no proporciona suficiente alteración de nombres.
Booboo
1

No estoy satisfecho con las dos respuestas anteriores para crear propiedades de solo lectura porque la primera solución permite eliminar el atributo de solo lectura y luego configurarlo y no bloquea el __dict__. La segunda solución podría resolverse con pruebas: encontrar el valor que sea igual al que estableció dos y cambiarlo eventualmente.

Ahora, por el 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()

No tiene sentido hacer atributos de solo lectura, excepto cuando escribe el código de la biblioteca, código que se distribuye a otros como código para usar con el fin de mejorar sus programas, no código para ningún otro propósito, como el desarrollo de aplicaciones. El problema de __dict__ está resuelto, porque el __dict__ ahora es de los tipos inmutables.MappingProxyType , por lo que los atributos no se pueden cambiar a través de __dict__. También se bloquea la configuración o eliminación de __dict__. La única forma de cambiar las propiedades de solo lectura es cambiando los métodos de la propia clase.

Aunque creo que mi solución es mejor que las dos anteriores, podría mejorarse. Estas son las debilidades de este código:

a) No permite agregar a un método en una subclase que establece o elimina un atributo de solo lectura. A un método definido en una subclase se le prohíbe automáticamente el acceso a un atributo de solo lectura, incluso al llamar a la versión del método de la superclase.

b) Los métodos de solo lectura de la clase se pueden cambiar para anular las restricciones de solo lectura.

Sin embargo, no hay forma sin editar la clase para establecer o eliminar un atributo de solo lectura. Esto no depende de las convenciones de nomenclatura, lo cual es bueno porque Python no es tan consistente con las convenciones de nomenclatura. Esto proporciona una forma de hacer atributos de solo lectura que no se pueden cambiar con lagunas ocultas sin editar la clase en sí. Simplemente enumere los atributos que se leerán solo cuando llame al decorador como argumentos y se convertirán en solo lectura.

Crédito a la respuesta de Brice en ¿Cómo obtener el nombre de la clase de la persona que llama dentro de una función de otra clase en Python? para obtener las clases y métodos de la persona que llama.

Miguel
fuente
object.__setattr__(pfi, 'readonly', 'foobar')rompe esta solución, sin editar la clase en sí.
L3viathan
0

Tenga en cuenta que los métodos de instancia también son atributos (de la clase) y que puede establecerlos a nivel de clase o instancia si realmente quisiera ser un rudo. O que puede establecer una variable de clase (que también es un atributo de la clase), donde las prácticas propiedades de solo lectura no funcionarán perfectamente de forma inmediata. Lo que estoy tratando de decir es que el problema del "atributo de solo lectura" es de hecho más general de lo que normalmente se percibe. Afortunadamente, existen expectativas convencionales en funcionamiento que son tan fuertes que nos cegan con respecto a estos otros casos (después de todo, casi todo es un atributo de algún tipo en Python).

Sobre la base de estas expectativas, creo que el enfoque más general y ligero es adoptar la convención de que los atributos "públicos" (sin subrayado inicial) son de solo lectura, excepto cuando se documentan explícitamente como escribibles. Esto subsume la expectativa habitual de que los métodos no se parchearán y las variables de clase que indican los valores predeterminados de la instancia son mejores y mucho menos. Si se siente realmente paranoico acerca de algún atributo especial, use un descriptor de solo lectura como última medida de recurso.

memeplex
fuente
0

Si bien me gusta el decorador de clases de Oz123, también puedes hacer lo siguiente, que usa un contenedor de clase explícito y __new__ con un método de fábrica de clases que devuelve la clase dentro de un cierre:

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()
Marqués de Apolo
fuente
debe agregar alguna prueba que muestre cómo funciona con múltiples propiedades
Oz123
0

Esa es mi solución.

@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
fuente
0

alguien mencionó el uso de un objeto proxy, no vi un ejemplo de eso, así que terminé probándolo, [mal].

/! \ Prefiera las definiciones de clase y los constructores de clase si es posible

este código se está reescribiendo efectivamente class.__new__(constructor de clases) excepto peor en todos los sentidos. Ahórrese el dolor y no utilice este patrón si puede.

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))
ThorSummoner
fuente
-2

Sé que estoy recuperando este hilo de la muerte, pero estaba buscando cómo hacer que una propiedad sea de solo lectura y, después de encontrar este tema, no estaba satisfecho con las soluciones ya compartidas.

Entonces, volviendo a la pregunta inicial, si comienza con este código:

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

Y si desea que X sea de solo lectura, puede agregar:

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

Entonces, si ejecuta lo siguiente:

print (x) # Will print whatever X value is
x = 3 # Will raise exception "Member readonly"
vincedjango
fuente
3
Pero si simplemente no crea un colocador, intentar asignar también generará un error (An AttributeError('can't set attribute'))
Artyer