¿Cuál es la forma pitónica de usar getters y setters?

341

Lo estoy haciendo así:

def set_property(property,value):  
def get_property(property):  

o

object.property = value  
value = object.property

Soy nuevo en Python, por lo que todavía estoy explorando la sintaxis, y me gustaría obtener algunos consejos para hacerlo.

Jorge Guberte
fuente

Respuestas:

684

Pruebe esto: Propiedad Python

El código de muestra es:

class C(object):
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """I'm the 'x' property."""
        print("getter of x called")
        return self._x

    @x.setter
    def x(self, value):
        print("setter of x called")
        self._x = value

    @x.deleter
    def x(self):
        print("deleter of x called")
        del self._x


c = C()
c.x = 'foo'  # setter called
foo = c.x    # getter called
del c.x      # deleter called
Grissiom
fuente
2
¿Se llama al configurador para x en el inicializador al instanciar _x?
Casey
77
@Casey: No. Las referencias a ._x(que no es una propiedad, solo un atributo simple) omiten el propertyajuste. Solo referencias para .xpasar por el property.
ShadowRanger
278

¿Cuál es la forma pitónica de usar getters y setters?

La forma "Pythonic" no es usar "getters" y "setters", sino usar atributos simples, como lo demuestra la pregunta, y delpara eliminar (pero los nombres se cambian para proteger a los inocentes ... builtins):

value = 'something'

obj.attribute = value  
value = obj.attribute
del obj.attribute

Si más tarde, desea modificar la configuración y la obtención, puede hacerlo sin tener que alterar el código de usuario, utilizando el propertydecorador:

class Obj:
    """property demo"""
    #
    @property            # first decorate the getter method
    def attribute(self): # This getter method name is *the* name
        return self._attribute
    #
    @attribute.setter    # the property decorates with `.setter` now
    def attribute(self, value):   # name, e.g. "attribute", is the same
        self._attribute = value   # the "value" name isn't special
    #
    @attribute.deleter     # decorate with `.deleter`
    def attribute(self):   # again, the method name is the same
        del self._attribute

(Cada uso de decorador copia y actualiza el objeto de propiedad anterior, así que tenga en cuenta que debe usar el mismo nombre para cada conjunto, obtener y eliminar función / método.

Después de definir lo anterior, la configuración original, obtener y eliminar el código es el mismo:

obj = Obj()
obj.attribute = value  
the_value = obj.attribute
del obj.attribute

Debes evitar esto:

def set_property(property,value):  
def get_property(property):  

En primer lugar, lo anterior no funciona, porque no proporciona un argumento para la instancia de que la propiedad se establecería en (generalmente self), que sería:

class Obj:

    def set_property(self, property, value): # don't do this
        ...
    def get_property(self, property):        # don't do this either
        ...

En segundo lugar, esto duplica el propósito de dos métodos especiales, __setattr__y __getattr__.

En tercer lugar, también tenemos las funciones incorporadas setattry getattr.

setattr(object, 'property_name', value)
getattr(object, 'property_name', default_value)  # default is optional

El @propertydecorador es para crear captadores y colocadores.

Por ejemplo, podríamos modificar el comportamiento de la configuración para colocar restricciones al valor que se establece:

class Protective(object):

    @property
    def protected_value(self):
        return self._protected_value

    @protected_value.setter
    def protected_value(self, value):
        if acceptable(value): # e.g. type or range check
            self._protected_value = value

En general, queremos evitar usar propertyy solo usar atributos directos.

Esto es lo que esperan los usuarios de Python. Siguiendo la regla de la menor sorpresa, debe intentar dar a sus usuarios lo que esperan a menos que tenga una razón muy convincente de lo contrario.

Demostración

Por ejemplo, supongamos que necesitamos que el atributo protegido de nuestro objeto sea un número entero entre 0 y 100 inclusive, y evitar su eliminación, con mensajes apropiados para informar al usuario sobre su uso adecuado:

class Protective(object):
    """protected property demo"""
    #
    def __init__(self, start_protected_value=0):
        self.protected_value = start_protected_value
    # 
    @property
    def protected_value(self):
        return self._protected_value
    #
    @protected_value.setter
    def protected_value(self, value):
        if value != int(value):
            raise TypeError("protected_value must be an integer")
        if 0 <= value <= 100:
            self._protected_value = int(value)
        else:
            raise ValueError("protected_value must be " +
                             "between 0 and 100 inclusive")
    #
    @protected_value.deleter
    def protected_value(self):
        raise AttributeError("do not delete, protected_value can be set to 0")

(Tenga en cuenta que se __init__refiere a self.protected_valuepero los métodos de propiedad se refieren self._protected_value. Esto es para que __init__use la propiedad a través de la API pública, asegurando que esté "protegida").

Y uso:

>>> p1 = Protective(3)
>>> p1.protected_value
3
>>> p1 = Protective(5.0)
>>> p1.protected_value
5
>>> p2 = Protective(-5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __init__
  File "<stdin>", line 15, in protected_value
ValueError: protectected_value must be between 0 and 100 inclusive
>>> p1.protected_value = 7.3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 17, in protected_value
TypeError: protected_value must be an integer
>>> p1.protected_value = 101
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 15, in protected_value
ValueError: protectected_value must be between 0 and 100 inclusive
>>> del p1.protected_value
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 18, in protected_value
AttributeError: do not delete, protected_value can be set to 0

¿Importan los nombres?

Si lo hacen . .settery .deleterhacer copias de la propiedad original. Esto permite que las subclases modifiquen correctamente el comportamiento sin alterar el comportamiento en el padre.

class Obj:
    """property demo"""
    #
    @property
    def get_only(self):
        return self._attribute
    #
    @get_only.setter
    def get_or_set(self, value):
        self._attribute = value
    #
    @get_or_set.deleter
    def get_set_or_delete(self):
        del self._attribute

Ahora para que esto funcione, debes usar los nombres respectivos:

obj = Obj()
# obj.get_only = 'value' # would error
obj.get_or_set = 'value'  
obj.get_set_or_delete = 'new value'
the_value = obj.get_only
del obj.get_set_or_delete
# del obj.get_or_set # would error

No estoy seguro de dónde sería útil, pero el caso de uso es si desea una propiedad get, set y / o delete-only. Probablemente sea mejor quedarse semánticamente con la misma propiedad que tenga el mismo nombre.

Conclusión

Comience con atributos simples.

Si más tarde necesita funcionalidad en torno a la configuración, la obtención y la eliminación, puede agregarla con el decorador de propiedades.

Evite las funciones nombradas set_...y get_..., para eso están las propiedades.

Aaron Hall
fuente
Además de duplicar la funcionalidad disponible, ¿por qué debería evitarse escribir sus propios setters y getters? Entiendo que podría no ser la forma pitónica, pero ¿hay problemas realmente serios que uno pueda encontrar de otra manera?
user1350191
44
En su demostración, el __init__método hace referencia self.protected_valuepero el getter y los setters hacen referencia self._protected_value. ¿Podría explicar cómo funciona esto? Probé su código y funciona como está, así que esto no es un error tipográfico.
codeforester
2
@codeforester Esperaba responder en mi respuesta antes, pero hasta que pueda, este comentario debería ser suficiente. Espero que pueda ver que utiliza la propiedad a través de la API pública, asegurando que esté "protegida". No tendría sentido "protegerlo" con una propiedad y luego usar la API no pública en su lugar __init__.
Aaron Hall
2
Sí, @AaronHall lo tiene ahora. No me di cuenta de self.protected_value = start_protected_valueque en realidad está llamando a la función setter; Pensé que era una tarea.
codeforester
1
En mi opinión, esta debería ser la respuesta aceptada, si entendí correctamente, Python toma el punto opuesto en comparación con, por ejemplo, Java. En lugar de hacer que todo sea privado de forma predeterminada y escribir un código adicional cuando sea necesario públicamente en Python, puede hacer que todo sea público y agregar privacidad más adelante
idclev 463035818
27
In [1]: class test(object):
    def __init__(self):
        self.pants = 'pants'
    @property
    def p(self):
        return self.pants
    @p.setter
    def p(self, value):
        self.pants = value * 2
   ....: 
In [2]: t = test()
In [3]: t.p
Out[3]: 'pants'
In [4]: t.p = 10
In [5]: t.p
Out[5]: 20
Autoplectic
fuente
17

El uso @propertyy le @attribute.setterayuda a usar no solo la forma "pitónica" sino también a verificar la validez de los atributos tanto al crear el objeto como al modificarlo.

class Person(object):
    def __init__(self, p_name=None):
        self.name = p_name

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

    @name.setter
    def name(self, new_name):
        if type(new_name) == str: #type checking for name property
            self._name = new_name
        else:
            raise Exception("Invalid value for name")

Con esto, realmente 'oculta' el _nameatributo de los desarrolladores del cliente y también realiza comprobaciones en el tipo de propiedad de nombre. Tenga en cuenta que siguiendo este enfoque, incluso durante el inicio, se llama al configurador. Entonces:

p = Person(12)

Dará lugar a:

Exception: Invalid value for name

Pero:

>>>p = person('Mike')
>>>print(p.name)
Mike
>>>p.name = 'George'
>>>print(p.name)
George
>>>p.name = 2.3 # Causes an exception
Farzad Vértigo
fuente
16

Mira el @propertydecorador .

Kevin Little
fuente
34
Esta es más o menos una respuesta de solo enlace.
Aaron Hall
66
@ AaronHall: Sin embargo, no lo es. "Elimine el marcado y aún obtendrá al menos un poco de información útil".
Jean-François Corbett
77
¿Cómo es esta una respuesta completa? Un enlace no es una respuesta.
Andy_A̷n̷d̷y̷
Creo que es una buena respuesta, porque la documentación allí establece claramente cómo usarla (y se mantendrá actualizada si cambia la implementación de Python, y dirige el OP al método que sugiere el respondedor. @ Jean-FrançoisCorbett declaró claramente "cómo" es una respuesta completa.
HunnyBear
En cualquier caso, esta respuesta no agrega nada útil a otras respuestas e idealmente debería eliminarse.
Georgy el
5

Puede usar accesores / mutadores (es decir, @attr.settery @property) o no, pero lo más importante es ser coherente.

Si está utilizando @propertypara acceder simplemente a un atributo, por ejemplo

class myClass:
    def __init__(a):
        self._a = a

    @property
    def a(self):
        return self._a

úsalo para acceder a todos los * atributos! Sería una mala práctica acceder a algunos atributos usando @propertyy dejar algunas otras propiedades públicas (es decir, nombre sin guión bajo) sin un descriptor de acceso, por ejemplo , no lo haga.

class myClass:
    def __init__(a, b):
        self.a = a
        self.b = b

    @property
    def a(self):
        return self.a

Tenga en cuenta que self.bno tiene un descriptor de acceso explícito aquí aunque sea público.

De manera similar con los setters (o mutadores ), siéntase libre de usar, ¡ @attribute.setterpero sea ​​consistente! Cuando lo haces, por ejemplo

class myClass:
    def __init__(a, b):
        self.a = a
        self.b = b 

    @a.setter
    def a(self, value):
        return self.a = value

Es difícil para mí adivinar tu intención. Por un lado, estás diciendo que ambos ay bson públicos (no hay un guión bajo en sus nombres), por lo que teóricamente se me debería permitir acceder / mutar (obtener / establecer) ambos. Pero luego especificas un mutador explícito solo para a, lo que me dice que tal vez no debería poder configurar b. Dado que ha proporcionado un mutador explícito, no estoy seguro de si la falta de un descriptor de acceso explícito ( @property) significa que no debería poder acceder a ninguna de esas variables o si simplemente estaba siendo frugal en el uso @property.

* La excepción es cuando desea explícitamente hacer que algunas variables sean accesibles o mutables, pero no ambas, o desea realizar alguna lógica adicional al acceder o mutar un atributo. Esto es cuando estoy usando personalmente @propertyy @attribute.setter(de lo contrario no hay acesores / mutadores explícitos para los atributos públicos).

Por último, PEP8 y sugerencias de la Guía de estilo de Google:

PEP8, Diseñando para herencia dice:

Para atributos de datos públicos simples, es mejor exponer solo el nombre del atributo, sin métodos complicados de acceso / mutación . Tenga en cuenta que Python proporciona un camino fácil para futuras mejoras, en caso de que un atributo de datos simple necesite crecer en el comportamiento funcional. En ese caso, use las propiedades para ocultar la implementación funcional detrás de la sintaxis de acceso a atributos de datos simple.

Por otro lado, de acuerdo con las Reglas / Propiedades del lenguaje Python de la Guía de estilo de Google, la recomendación es:

Utilice las propiedades en el nuevo código para acceder o establecer datos donde normalmente hubiera utilizado métodos simples o ligeros de acceso o configuración. Las propiedades deben crearse con el @propertydecorador.

Los pros de este enfoque:

La legibilidad aumenta al eliminar las llamadas explícitas a métodos get y set para un acceso simple a los atributos. Permite que los cálculos sean flojos. Considerada la forma pitónica de mantener la interfaz de una clase. En términos de rendimiento, permitir que las propiedades omitan la necesidad de métodos de acceso triviales cuando un acceso variable directo es razonable. Esto también permite agregar métodos de acceso en el futuro sin romper la interfaz.

y contras:

Debe heredar de objectPython 2. Puede ocultar los efectos secundarios al igual que la sobrecarga del operador. Puede ser confuso para las subclases.

Tomasz Bartkowiak
fuente
1
Estoy totalmente en desacuerdo. Si tengo 15 atributos en mi objeto, y quiero que se calcule uno @property, hacer que el resto también lo use @propertyparece una mala decisión.
Quelklef
De acuerdo, pero solo si hay algo específico sobre este atributo particular que necesita @property(por ejemplo, ejecutar alguna lógica especial antes de devolver un atributo). De lo contrario, ¿por qué decorarías un atributo @properyy no otros?
Tomasz Bartkowiak
@Quelklef vea la nota al margen en la publicación (marcada con un asterisco).
Tomasz Bartkowiak
Bueno ... Si no estás haciendo una de las cosas mencionadas en la nota al margen, entonces no deberías estar usando @propertypara empezar, ¿verdad? Si tu getter es return this._xy tu setter es this._x = new_x, entonces usarlo @propertyes un poco tonto.
Quelklef
1
Hmm, tal vez. Yo personalmente diría que no está bien, es completamente superfluo. Pero puedo ver de dónde vienes. Supongo que acabo de leer su publicación diciendo que "lo más importante cuando se usa @propertyes ser consistente".
Quelklef
-1

Puedes usar los métodos mágicos __getattribute__y __setattr__.

class MyClass:
    def __init__(self, attrvalue):
        self.myattr = attrvalue
    def __getattribute__(self, attr):
        if attr == "myattr":
            #Getter for myattr
    def __setattr__(self, attr):
        if attr == "myattr":
            #Setter for myattr

Tenga en cuenta que __getattr__y __getattribute__no son lo mismo. __getattr__solo se invoca cuando no se encuentra el atributo.

Pika el mago de las ballenas
fuente