Pregunta Usar @property versus getters y setters


Aquí hay una pregunta de diseño específica de Python:

class MyClass(object):
    ...
    def get_my_attr(self):
        ...

    def set_my_attr(self, value):
        ...

y

class MyClass(object):
    ...        
    @property
    def my_attr(self):
        ...

    @my_attr.setter
    def my_attr(self, value):
        ...

Python nos permite hacerlo de cualquier manera. Si diseña un programa de Python, ¿qué enfoque usaría y por qué?


636
2017-07-07 22:42


origen


Respuestas:


Prefiere propiedades. Es para lo que están allí.

La razón es que todos los atributos son públicos en Python. Los nombres iniciales con un guión bajo o dos son solo una advertencia de que el atributo dado es un detalle de implementación que puede no ser el mismo en futuras versiones del código. No impide que consigas o establezcas ese atributo. Por lo tanto, el acceso a atributos estándar es la forma normal y pitonica de, bueno, acceder a los atributos.

La ventaja de las propiedades es que son sintácticamente idénticas al acceso a los atributos, por lo que puede cambiar de una a otra sin ningún cambio en el código del cliente. Incluso podría tener una versión de una clase que use propiedades (por ejemplo, para código por contrato o depuración) y otra que no sea para producción, sin cambiar el código que la usa. Al mismo tiempo, no es necesario que escriba getters y setters para todo, en caso de que necesite controlar mejor el acceso más adelante.


545
2017-07-07 23:06



En Python no usas getters o setters o propiedades solo por el gusto de hacerlo. Primero solo usa atributos y luego, solo si es necesario, eventualmente migrará a una propiedad sin tener que cambiar el código usando sus clases.

De hecho, hay una gran cantidad de código con extensión .py que utiliza getters y setters y herencia y clases sin sentido en todas partes donde, p. una tupla simple sería suficiente, pero es código de gente escribiendo en C ++ o Java usando Python.

Eso no es código Python.


135
2017-07-07 23:08



El uso de propiedades le permite comenzar con los accesos de atributos normales y luego respaldarlos con getters y setters luego según sea necesario.


112
2017-07-07 22:57



La respuesta corta es: propiedades gana sin problemas. Siempre.

A veces hay necesidad de getters y setters, pero incluso entonces, los "ocultaría" al mundo exterior. Hay muchas formas de hacerlo en Python (getattr, setattr, __getattribute__, etc ..., pero uno muy conciso y limpio es:

def set_email(self, value):
    if '@' not in value:
        raise Exception("This doesn't look like an email address.")
    self._email = value

def get_email(self):
    return self._email

email = property(get_email, set_email)

Aquí hay un breve artículo que introduce el tema de getters y setters en Python.


65
2017-07-07 22:52



[TL; DR?  Usted puede saltar al final para ver un ejemplo de código.]

De hecho, prefiero usar un idioma diferente, que es un poco complicado para usarlo como un único, pero es bueno si tienes un caso de uso más complejo.

Un poco de historia primero.

Las propiedades son útiles, ya que nos permiten manejar tanto la configuración como la obtención de valores de forma programática, pero aún permiten acceder a los atributos como atributos. Podemos convertir 'get' en 'computaciones' (esencialmente) y podemos convertir 'sets' en 'events'. Entonces digamos que tenemos la siguiente clase, que he codificado con getters y setters similares a Java.

class Example(object):
    def __init__(self, x=None, y=None):
        self.x = x
        self.y = y

    def getX(self):
        return self.x or self.defaultX()

    def getY(self):
        return self.y or self.defaultY()

    def setX(self, x):
        self.x = x

    def setY(self, y):
        self.y = y

    def defaultX(self):
        return someDefaultComputationForX()

    def defaultY(self):
        return someDefaultComputationForY()

Usted se estará preguntando por qué no llame defaultX y defaultY en el objeto __init__método. La razón es que, para nuestro caso, quiero suponer que el someDefaultComputation los métodos devuelven valores que varían con el tiempo, por ejemplo, una marca de tiempo y cada vez que x (o y) no está configurado (donde, a los fines de este ejemplo, "no establecido" significa "configurado en Ninguno") Quiero el valor de xes (o y's) cálculo predeterminado.

Así que esto es cojo por una serie de razones descritas anteriormente. Lo reescribiré usando propiedades:

class Example(object):
    def __init__(self, x=None, y=None):
        self._x = x
        self._y = y

    @property
    def x(self):
        return self.x or self.defaultX()

    @x.setter
    def x(self, value):
        self._x = value

    @property
    def y(self):
        return self.y or self.defaultY()

    @y.setter
    def y(self, value):
        self._y = value

    # default{XY} as before.

¿Qué hemos ganado? Hemos obtenido la capacidad de referirnos a estos atributos como atributos aunque, detrás de escena, terminemos ejecutando métodos.

Por supuesto, el poder real de las propiedades es que generalmente queremos que estos métodos hagan algo además de solo obtener y establecer valores (de lo contrario, no tiene sentido usar propiedades). Hice esto en mi ejemplo getter. Básicamente estamos ejecutando un cuerpo de función para elegir un valor predeterminado cuando el valor no está establecido. Este es un patrón muy común.

Pero, ¿qué estamos perdiendo y qué no podemos hacer?

La principal molestia, en mi opinión, es que si defines un getter (como lo hacemos aquí) también tienes que definir un setter. [1] Eso es un ruido extra que desordena el código.

Otra molestia es que todavía tenemos que inicializar el x y y valores en __init__. (Bueno, por supuesto que podríamos agregarlos usando setattr() pero ese es un código extra).

En tercer lugar, a diferencia del ejemplo de Java, los getters no pueden aceptar otros parámetros. Ahora puedo oírtelo decir, bueno, si está tomando parámetros, ¡no es un getter! En un sentido oficial, eso es cierto. Pero en un sentido práctico no hay ninguna razón por la cual no podamos parametrizar un atributo con nombre, como x - y establecer su valor para algunos parámetros específicos.

Sería bueno si pudiéramos hacer algo como:

e.x[a,b,c] = 10
e.x[d,e,f] = 20

por ejemplo. Lo más cerca que podemos estar es anular la tarea para implicar alguna semántica especial:

e.x = [a,b,c,10]
e.x = [d,e,f,30]

y, por supuesto, asegúrese de que nuestro colocador sepa cómo extraer los tres primeros valores como clave de un diccionario y establecer su valor en un número o algo.

Pero incluso si lo hiciéramos, no podríamos soportarlo con propiedades porque no hay forma de obtener el valor porque no podemos pasar los parámetros al getter. Así que tuvimos que devolver todo, presentando una asimetría.

El getter / setter al estilo Java nos permite manejar esto, pero volvemos a necesitar getter / setters.

En mi opinión, lo que realmente queremos es algo que capture los siguientes requisitos:

  • Los usuarios definen solo un método para un atributo dado y pueden indicar allí si el atributo es de solo lectura o de lectura / escritura. Las propiedades no pasan esta prueba si el atributo puede escribirse

  • No es necesario que el usuario defina una variable adicional que subyace a la función, por lo que no necesitamos el __init__ o setattr en el código. La variable solo existe por el hecho de que hemos creado este atributo de estilo nuevo.

  • Cualquier código predeterminado para el atributo se ejecuta en el propio cuerpo del método.

  • Podemos establecer el atributo como un atributo y referenciarlo como un atributo.

  • Podemos parametrizar el atributo.

En términos de código, queremos una forma de escribir:

def x(self, *args):
    return defaultX()

y poder hacer luego:

print e.x     -> The default at time T0
e.x = 1
print e.x     -> 1
e.x = None
print e.x     -> The default at time T1

Etcétera.

También queremos una manera de hacer esto para el caso especial de un atributo parametrizable, pero aún así permitir que funcione el caso de asignación predeterminado. Verás cómo he abordado esto a continuación.

Ahora al grano (¡yay! ¡El punto!). La solución que surgió para esto es la siguiente.

Creamos un nuevo objeto para reemplazar la noción de una propiedad. El objetivo del objeto es almacenar el valor de una variable establecida en él, pero también mantiene un identificador en el código que sabe cómo calcular un valor predeterminado. Su trabajo es almacenar el conjunto value o para ejecutar el method si ese valor no está establecido.

Vamos a llamarlo UberProperty.

class UberProperty(object):

    def __init__(self, method):
        self.method = method
        self.value = None
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def clearValue(self):
        self.value = None
        self.isSet = False

Asumo method aquí hay un método de clase, value es el valor de la UberPropertyy he agregado isSet porque None puede ser un valor real y esto nos permite una manera limpia de declarar que realmente "no tiene valor". Otra forma es un centinela de algún tipo.

Esto básicamente nos da un objeto que puede hacer lo que queremos, pero ¿cómo lo ponemos en nuestra clase? Bueno, las propiedades usan decoradores; por qué no podemos? Veamos cómo se vería (a partir de ahora me limitaré a usar un solo 'atributo', x)

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

Esto en realidad no funciona todavía, por supuesto. Tenemos que implementar uberProperty y asegúrese de que maneja tanto get como sets.

Comencemos con consigue.

Mi primer intento fue simplemente crear un nuevo objeto UberProperty y devolverlo:

def uberProperty(f):
    return UberProperty(f)

Descubrí rápidamente, por supuesto, que esto no funciona: Python nunca une el llamable al objeto y necesito el objeto para llamar a la función. Incluso la creación del decorador en la clase no funciona, ya que aunque ahora tenemos la clase, todavía no tenemos un objeto para trabajar.

Entonces vamos a necesitar poder hacer más aquí. Sabemos que un método solo se debe representar una vez, así que sigamos y guardemos nuestro decorador, pero modifiquemos UberProperty para solo almacenar el method referencia:

class UberProperty(object):

    def __init__(self, method):
        self.method = method

Tampoco se puede llamar, por lo que en este momento no funciona nada.

¿Cómo completamos la imagen? Bueno, ¿con qué terminamos cuando creamos la clase de ejemplo usando nuestro nuevo decorador?

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

print Example.x     <__main__.UberProperty object at 0x10e1fb8d0>
print Example().x   <__main__.UberProperty object at 0x10e1fb8d0>

en ambos casos, recuperamos el UberProperty que, por supuesto, no es invocable, por lo que no sirve de mucho.

Lo que necesitamos es una forma de vincular dinámicamente el UberProperty instancia creada por el decorador después de que la clase se haya creado a un objeto de la clase antes de que ese objeto haya sido devuelto a ese usuario para su uso. Um, sí, eso es un __init__ llama, amigo

Escribamos lo que queremos que nuestro resultado sea primero. Estamos vinculando un UberProperty a una instancia, entonces una cosa obvia para devolver sería una BoundUberProperty. Aquí es donde realmente mantendremos el estado para el x atributo.

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

Ahora nosotros la representación; ¿cómo conseguir esto en un objeto? Hay algunos enfoques, pero el más fácil de explicar solo usa el __init__método para hacer ese mapeo. Para el momento __init__ se llama nuestros decoradores se han ejecutado, por lo que solo tiene que mirar a través de los objetos __dict__ y actualice cualquier atributo donde el valor del atributo sea de tipo UberProperty.

Ahora, las súper-propiedades son geniales y probablemente querremos usarlas mucho, por lo que tiene sentido crear una clase base que haga esto para todas las subclases. Creo que sabes cómo se llamará a la clase base.

class UberObject(object):
    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)

Añadimos esto, cambiamos nuestro ejemplo para heredar de UberObjecty ...

e = Example()
print e.x               -> <__main__.BoundUberProperty object at 0x104604c90>

Después de modificar x ser:

@uberProperty
def x(self):
    return *datetime.datetime.now()*

Podemos ejecutar una prueba simple:

print e.x.getValue()
print e.x.getValue()
e.x.setValue(datetime.date(2013, 5, 31))
print e.x.getValue()
e.x.clearValue()
print e.x.getValue()

Y obtenemos la salida que queríamos:

2013-05-31 00:05:13.985813
2013-05-31 00:05:13.986290
2013-05-31
2013-05-31 00:05:13.986310

(Vaya, estoy trabajando hasta tarde.)

Tenga en cuenta que he usado getValue, setValuey clearValue aquí. Esto se debe a que aún no me he vinculado en los medios para que estos se devuelvan automáticamente.

Pero creo que este es un buen lugar para parar por ahora, porque me estoy cansando. También puede ver que la funcionalidad principal que queríamos está en su lugar; el resto es escaparate. Ventanilla de usabilidad importante, pero eso puede esperar hasta que tenga un cambio para actualizar la publicación.

Terminaré el ejemplo en la siguiente publicación abordando estas cosas:

  • Necesitamos asegurarnos de que UberObject __init__ siempre es llamado por subclases.

    • Entonces forzamos que se llame en alguna parte o evitamos que se implemente.
    • Veremos cómo hacer esto con una metaclase.
  • Necesitamos asegurarnos de que manejamos el caso común donde alguien 'alias' una función para otra cosa, como:

      class Example(object):
          @uberProperty
          def x(self):
              ...
    
          y = x
    
  • Necesitamos e.x regresar e.x.getValue() por defecto.

    • Lo que realmente veremos es que esta es un área donde el modelo falla.
    • Resulta que siempre necesitaremos usar una llamada de función para obtener el valor.
    • Pero podemos hacer que se vea como una llamada a función normal y evitar tener que usar e.x.getValue(). (Hacer esto es obvio, si aún no lo ha solucionado).
  • Necesitamos apoyar la configuración e.x directly, como en e.x = <newvalue>. También podemos hacer esto en la clase para padres, pero tendremos que actualizar nuestra __init__ código para manejarlo.

  • Finalmente, agregaremos atributos parametrizados. Debería ser bastante obvio cómo haremos esto también.

Aquí está el código tal como existe hasta ahora:

import datetime

class UberObject(object):
    def uberSetter(self, value):
        print 'setting'

    def uberGetter(self):
        return self

    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)


class UberProperty(object):
    def __init__(self, method):
        self.method = method

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

    def uberProperty(f):
        return UberProperty(f)

class Example(UberObject):

    @uberProperty
    def x(self):
        return datetime.datetime.now()

[1] Puedo estar atrasado en si este sigue siendo el caso.


53
2018-05-31 04:27



Creo que ambos tienen su lugar. Un problema con el uso @property es que es difícil extender el comportamiento de getters o setters en subclases utilizando mecanismos de clase estándar. El problema es que las funciones reales getter / setter están ocultas en la propiedad.

En realidad puede hacerse cargo de las funciones, p. con

class C(object):
    _p = 1
    @property
    def p(self):
        return self._p
    @p.setter
    def p(self, val):
        self._p = val

puede acceder a las funciones getter y setter como C.p.fget y C.p.fset, pero no puede usar fácilmente las instalaciones normales de herencia de métodos (por ejemplo, super) para ampliarlas. Después de profundizar en las complejidades de Super, tu poder de hecho, use super de esta manera:

# Using super():
class D(C):
    # Cannot use super(D,D) here to define the property
    # since D is not yet defined in this scope.
    @property
    def p(self):
        return super(D,D).p.fget(self)

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for D'
        super(D,D).p.fset(self, val)

# Using a direct reference to C
class E(C):
    p = C.p

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for E'
        C.p.fset(self, val)

El uso de super () es, sin embargo, bastante torpe, ya que la propiedad tiene que redefinirse, y tienes que usar el mecanismo súper contra intuitivo (cls, cls) para obtener una copia de p sin consolidar.


24
2017-12-23 12:09



Usar propiedades es para mí más intuitivo y se adapta mejor a la mayoría del código.

Comparando

o.x = 5
ox = o.x

vs.

o.setX(5)
ox = o.getX()

es para mí bastante obvio, y es más fácil de leer. También las propiedades permiten que las variables privadas sean mucho más fáciles.


19
2017-07-07 22:53