setters - python 3 class property




Usando @property contra getters y setters (8)

Aquí hay una pregunta de diseño específico 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ñara un programa Python, ¿qué enfoque usaría y por qué?


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

Puedes conseguir las funciones, por ejemplo, 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 de Cpfget y Cpfset como Cpfget y Cpfset , pero no puede usar fácilmente las facilidades de herencia de métodos normales (por ejemplo, super) para ampliarlas. Después de profundizar en las complejidades de super, puedes usar 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)

Sin embargo, el uso de super () es bastante torpe, ya que la propiedad tiene que ser redefinida, y usted tiene que usar el mecanismo de super (cls, cls) ligeramente contraintuitivo para obtener una copia no enlazada de p.



En proyectos complejos, prefiero usar propiedades de solo lectura (o captadores) con función de establecimiento explícito:

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

def set_my_attr(self, value):
    ...

En proyectos de larga vida, la depuración y refactorización lleva más tiempo que escribir el código en sí. Hay varias desventajas para usar @property.setter que hace que la depuración sea aún más difícil:

1) Python permite crear nuevos atributos para un objeto existente. Esto hace que una siguiente errata sea difícil de rastrear:

my_object.my_atttr = 4.

Si su objeto es un algoritmo complicado, entonces pasará bastante tiempo tratando de averiguar por qué no converge (observe una 't' adicional en la línea de arriba)

2) el configurador a veces puede evolucionar hacia un método lento y complicado (por ejemplo, golpear una base de datos). Sería bastante difícil para otro desarrollador descubrir por qué la siguiente función es muy lenta. Podría dedicar mucho tiempo a perfilar el método do_something() :

def slow_function(my_object):
    my_object.my_attr = 4.
    my_object.do_something()

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

A veces hay una necesidad de captadores y definidores, pero incluso así, los "escondería" al mundo exterior. Hay muchas formas de hacer esto en Python ( getattr , setattr , __getattribute__ , etc ..., pero una muy concisa y limpia 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 presenta el tema de captadores y configuradores en Python.


Preferiría no usar ninguno de los dos en la mayoría de los casos El problema con las propiedades es que hacen que la clase sea menos transparente. Especialmente, este es un problema si tuviera que generar una excepción de un setter. Por ejemplo, si tiene una propiedad Account.email:

class Account(object):
    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, value):
        if '@' not in value:
            raise ValueError('Invalid email address.')
        self._email = value

entonces el usuario de la clase no espera que la asignación de un valor a la propiedad pueda causar una excepción:

a = Account()
a.email = 'badaddress'
--> ValueError: Invalid email address.

Como resultado, la excepción puede no ser manejada y propagarse demasiado alto en la cadena de llamadas para ser manejado adecuadamente, o dar como resultado una respuesta muy inútil al usuario del programa (lo cual, lamentablemente, es muy común en el mundo de python y java). ).

También evitaría el uso de getters y setters:

  • porque definirlos por adelantado para todas las propiedades requiere mucho tiempo,
  • hace que la cantidad de código sea innecesariamente más larga, lo que dificulta la comprensión y el mantenimiento del código,
  • Si los definiera para las propiedades solo cuando fuera necesario, la interfaz de la clase cambiaría, perjudicando a todos los usuarios de la clase.

En lugar de propiedades y getters / setters prefiero hacer la lógica compleja en lugares bien definidos, como en un método de validación:

class Account(object):
    ...
    def validate(self):
        if '@' not in self.email:
            raise ValueError('Invalid email address.')

o un método similar de Account.save.

Tenga en cuenta que no estoy tratando de decir que no hay casos en los que las propiedades sean útiles, solo que puede estar mejor si puede hacer que sus clases sean lo suficientemente simples y transparentes para que no las necesite.


Siento que las propiedades son acerca de permitirte obtener la sobrecarga de escribir a los captadores y definidores solo cuando realmente los necesitas.

La cultura de programación de Java recomienda encarecidamente no dar nunca acceso a las propiedades, y en su lugar, ir a través de captadores y configuradores, y solo aquellos que realmente son necesarios. Es un poco detallado escribir siempre estos fragmentos de código obvios, y notar que el 70% del tiempo nunca son reemplazados por alguna lógica no trivial.

En Python, la gente realmente se preocupa por ese tipo de sobrecarga, de modo que puedes adoptar la siguiente práctica:

  • No use captadores y definidores al principio, cuando no sean necesarios.
  • Use @property para implementarlas sin cambiar la sintaxis del resto de su código.

Usar propiedades es para mí más intuitivo y se adapta mejor a la mayoría de los códigos.

Comparando

o.x = 5
ox = o.x

contra

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

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


[ TL; DR? Puedes saltar al final de un ejemplo de código .]

De hecho, prefiero usar un idioma diferente, lo cual es un poco complicado para usarlo solo, pero es bueno si tienes un caso de uso más complejo.

Un poco de historia primero.

Las propiedades son útiles porque nos permiten manejar tanto la configuración como la obtención de valores de una manera programática, pero aún así permiten que se pueda acceder a los atributos como atributos. Podemos convertir 'get' en 'cálculos' (esencialmente) y podemos convertir 'sets' en 'eventos'. Así que digamos que tenemos la siguiente clase, la cual 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()

Quizás se esté preguntando por qué no llamé defaultX y defaultY en el método __init__ del objeto. La razón es que, en nuestro caso, quiero asumir que los métodos someDefaultComputation devuelven valores que varían con el tiempo, por ejemplo, una marca de tiempo, y siempre que no se establece x (o y ) (donde, para el propósito de este ejemplo, "no se establece" significa "establecido en Ninguno") Quiero el valor del cálculo predeterminado de x (o y ).

Así que esto es escaso por una serie de razones descritas anteriormente. Lo reescribiré usando las 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 ganado la capacidad de referirnos a estos atributos como atributos a pesar de que, detrás de la escena, terminamos ejecutando métodos.

Por supuesto, el poder real de las propiedades es que generalmente queremos que estos métodos hagan algo además de 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 seleccionar un valor predeterminado cuando no se establece el valor. 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 define un captador (como lo hacemos aquí) también tiene que definir un definidor. [1] Eso es ruido extra que desordena el código.

Otra molestia es que todavía tenemos que inicializar los valores de x e y en __init__ . (Bueno, por supuesto que podríamos agregarlos usando setattr() pero eso es más código adicional).

Tercero, a diferencia del ejemplo similar a Java, los captadores no pueden aceptar otros parámetros. Ahora puedo oírte decir, bueno, si está tomando parámetros, ¡no es un captador! En un sentido oficial, eso es cierto. Pero en un sentido práctico, no hay razón para que 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 cercano que podemos llegar es anular la asignación para implicar algunas semánticas especiales:

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

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

Pero incluso si lo hiciéramos, todavía no podríamos admitirlo con propiedades porque no hay manera de obtener el valor porque no podemos pasar parámetros al getter. Así que hemos tenido que devolver todo, introduciendo una asimetría.

El getter / setter de 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 si el atributo es de solo lectura o de lectura y escritura. Las propiedades fallan esta prueba si el atributo se puede escribir.

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

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

  • Podemos establecer el atributo como un atributo y hacer referencia a él 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 entonces hacer:

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 forma de hacerlo para el caso especial de un atributo parametrizable, pero aún así permitimos que el caso de asignación por defecto funcione. Verás cómo abordé esto a continuación.

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

Creamos un nuevo objeto para reemplazar la noción de una propiedad. El objeto está destinado a 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 value establecido o ejecutar el method si ese valor no está establecido.

Llamémoslo 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

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

Básicamente, esto nos da un objeto que puede hacer lo que queremos, pero ¿cómo lo ponemos en nuestra clase? Bueno, las propiedades usan decoradores; por que no podemos Veamos cómo podría verse (de aquí en adelante me atreveré a usar solo un 'atributo', x ).

class Example(object):

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

Esto no funciona en realidad todavía, por supuesto. Tenemos que implementar uberProperty y asegurarnos de que maneje tanto las uberProperty como los conjuntos.

Vamos a empezar con obtiene.

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

def uberProperty(f):
    return UberProperty(f)

Por supuesto, rápidamente descubrí que esto no funciona: Python nunca vincula el objeto invocable 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 con el que trabajar.

Así que vamos a necesitar poder hacer más aquí. Sabemos que un método solo debe representarse una vez, así que sigamos adelante y conservemos nuestro decorador, pero modifiquemos UberProperty para almacenar solo la referencia del method :

class UberProperty(object):

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

Tampoco es llamable, por lo que en este momento nada está funcionando.

¿Cómo completamos el cuadro? Bueno, ¿con qué terminamos cuando creamos la clase de ejemplo utilizando 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 la UberProperty que por supuesto no es invocable, por lo que no es de mucha utilidad.

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

Vamos a escribir lo que queremos que nuestro resultado de búsqueda sea primero. Estamos vinculando una UberProperty a una instancia, por lo que una cosa obvia para devolver sería una BoundUberProperty. Aquí es donde realmente mantendremos el estado para el atributo x .

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 llevar esto a un objeto? Hay algunos enfoques, pero el más fácil de explicar solo usa el método __init__ para hacer ese mapeo. En el momento en que __init__ se llama nuestros decoradores se han ejecutado, así que solo hay que revisar el __dict__ del objeto y actualizar cualquier atributo donde el valor del atributo sea de tipo UberProperty .

Ahora, las súper-propiedades son geniales y probablemente querremos usarlas mucho, así que tiene sentido crear una clase base que haga esto para todas las subclases. Creo que sabes cómo se llamará 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)

UberObject esto, cambiamos nuestro ejemplo para heredar de UberObject , y ...

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

Después de modificar x para ser:

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

Podemos realizar una prueba sencilla:

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 conseguimos 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

(Caramba, estoy trabajando hasta tarde).

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

Pero creo que este es un buen lugar para detenerse 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. Importante ventana de utilidad, pero eso puede esperar hasta que tenga un cambio para actualizar la publicación.

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

  • Necesitamos asegurarnos de que las __init__ siempre llaman a __init__ de __init__ .

    • Entonces, o bien obligamos a que se llame a algún lugar o impidimos que se implemente.
    • Veremos cómo hacer esto con una metaclase.
  • Debemos asegurarnos de que manejamos el caso común en el que alguien "asigna un alias" de una función a otra cosa, como por ejemplo:

      class Example(object):
          @uberProperty
          def x(self):
              ...
    
          y = x
    
  • Necesitamos ex para devolver exgetValue() 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 de función regular y evitar tener que usar exgetValue() . (Hacer esto es obvio, si aún no lo has solucionado).
  • Necesitamos apoyar la configuración ex directly , como en ex = <newvalue> . También podemos hacer esto en la clase principal, pero necesitaremos actualizar nuestro código __init__ para manejarlo.

  • Finalmente, añadiremos 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.





getter-setter