Usando @property contra getters y setters

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 progtwig de Python, ¿qué enfoque usaría y por qué?

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

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 le impide realmente obtener o establecer ese atributo. Por lo tanto, el acceso a los atributos estándar es la forma normal, en Pythonic, de 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, código por contrato o depuración) y una que no lo haga para producción, sin cambiar el código que lo usa. Al mismo tiempo, no tiene que escribir getters y setters para todo, en caso de que necesite controlar mejor el acceso más adelante.

En Python, no usas captadores, definidores o propiedades solo por diversión. Primero solo usa los atributos y luego, solo si es necesario, eventualmente migra a una propiedad sin tener que cambiar el código usando sus clases.

De hecho, hay una gran cantidad de código con la extensión .py que utiliza captadores y definidores y clases de herencia y sin sentido en todas partes donde, por ejemplo, una tupla simple haría, pero es código de personas que escriben en C ++ o Java usando Python.

Eso no es el código de Python.

El uso de propiedades le permite comenzar con los accesos de atributos normales y, luego, hacer una copia de seguridad con los que obtienen y configuran, según sea necesario .

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

A veces hay una necesidad de captadores y establecedores, 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 definidores en Python.

[ 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 como una sola vez, 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 los atributos sean accedidos como atributos. Podemos convertir ‘get’ en ‘cálculos’ (esencialmente) y podemos convertir ‘sets’ en ‘eventos’. Así que digamos que tenemos la siguiente clase, que he codificado con los que obtienen y configuran como los de 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, a los efectos de este ejemplo, “no se establece” significa “establecido en Ninguno”) Quiero el valor del cálculo predeterminado de x (o de y ‘s).

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, entre bastidores, 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 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 seleccionar 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 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:

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

por ejemplo. Lo más cercano que podemos llegar es anular la asignación para implicar algunas semánticas especiales:

 ex = [a,b,c,10] ex = [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 hiciéramos eso, 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 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 ex -> The default at time T0 ex = 1 print ex -> 1 ex = None print ex -> 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 

Asumo 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; porque 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 bash 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 alguna 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 es usar el método __init__ para hacer ese mapeo. Cuando se llama a __init__ nuestros decoradores ya se han ejecutado, así que solo hay que revisar el __dict__ del objeto y actualizar los atributos 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 ex -> <__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 exgetValue() print exgetValue() exsetValue(datetime.date(2013, 5, 31)) print exgetValue() exclearValue() print exgetValue() 

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 rest es escaparate. Importante ventana de usabilidad, 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 invocan el __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 arreglado).
  • Necesitamos apoyar la configuración ex directly , como en ex = . 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.

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 = Cp @p.setter def p(self, val): print 'Implement extra functionality here for E' Cpfset(self, val) 

Sin embargo, el uso de super () es bastante torpe, ya que la propiedad se debe redefinir, y se debe usar el mecanismo de super (cls, cls) ligeramente contraintuitivo para obtener una copia no unida de p.

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

Comparando

 ox = 5 ox = ox 

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.

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 progtwig (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 que las propiedades sean útiles, solo que puede que esté mejor si puede hacer que sus clases sean lo suficientemente simples y transparentes para que no las necesite.

Me sorprende que nadie haya mencionado que las propiedades son métodos limitados de una clase de descriptores, Adam Donohue y NeilenMarais entienden exactamente esta idea en sus publicaciones, que los captadores y definidores son funciones y pueden utilizarse para:

  • validar
  • alterar datos
  • tipo de pato (tipo de coacción a otro tipo)

Esto presenta una forma inteligente de ocultar los detalles de la implementación y el desplazamiento de código como expresiones regulares, tipo de conversión, prueba … excepto bloques, aserciones o valores computados.

En general, hacer CRUD en un objeto a menudo puede ser bastante mundano, pero considere el ejemplo de los datos que se conservarán en una base de datos relacional. Los ORM pueden ocultar los detalles de la implementación de determinados lenguajes vernáculos en los métodos vinculados a fget, fset, fdel definido en una clase de propiedad que administrará las terribles escalas if .. elif .. else que son tan feas en el código OO, exponiendo lo simple y elegant self.variable = something y obviar los detalles para el desarrollador utilizando el ORM.

Si uno piensa en las propiedades solo como un vestigio sombrío de un lenguaje de Bondage y Disciplina (es decir, Java), les falta el punto de descriptores.

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 progtwigció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 una 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 utilice captadores y definidores al principio, cuando no sean necesarios.
  • Use @property para implementarlas sin cambiar la syntax del rest de su código.

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 varios inconvenientes para el uso de @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 el seguimiento erróneo sea muy difícil de rastrear:

 my_object.my_atttr = 4. 

Si su objeto es un algoritmo complicado, entonces pasará bastante tiempo tratando de descubrir 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() , mientras que my_object.my_attr = 4. es en realidad la causa de la desaceleración:

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

Tanto la @property como la tradicional y la establecida tienen sus ventajas. Depende de su caso de uso.

Ventajas de @property

  • No es necesario cambiar la interfaz al cambiar la implementación del acceso a datos. Cuando su proyecto es pequeño, es probable que desee usar el acceso directo a los atributos para acceder a un miembro de la clase. Por ejemplo, supongamos que tiene un objeto foo de tipo Foo , que tiene un número de miembro. Entonces simplemente puede obtener este miembro con num = foo.num . A medida que su proyecto crezca, puede sentir que debe haber algunas comprobaciones o errores en el acceso a atributos simples. Luego puedes hacer eso con una @property dentro de la clase. La interfaz de acceso a los datos sigue siendo la misma, por lo que no es necesario modificar el código del cliente.

    Citado de PEP-8 :

    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 necesite un atributo de datos simple para boost el comportamiento funcional. En ese caso, use las propiedades para ocultar la implementación funcional detrás de la syntax de acceso a atributos de datos simples.

  • El uso de @property para el acceso a datos en Python se considera Pythonic :

    • Puede fortalecer su autoidentificación como progtwigdor de Python (no de Java).

    • Puede ayudar a su entrevista de trabajo si su entrevistador piensa que los que obtienen y configuran el estilo de Java son anti-patrones .

Ventajas de los tradicionales getters y setters.

  • Los captadores y definidores tradicionales permiten un acceso a los datos más complicado que el acceso a atributos simples. Por ejemplo, cuando está configurando un miembro de la clase, a veces necesita una marca que indique dónde le gustaría forzar esta operación, incluso si algo no se ve perfecto. Si bien no es obvio cómo boost el acceso de un miembro directo como foo.num = num , puede boost fácilmente su configurador tradicional con un parámetro de force adicional:

     def Foo: def set_num(self, num, force=False): ... 
  • Los getters y setters tradicionales hacen explícito que el acceso de un miembro de la clase es a través de un método. Esto significa:

    • Lo que obtiene como resultado puede no ser el mismo que se almacena exactamente dentro de esa clase.

    • Incluso si el acceso parece un acceso de atributo simple, el rendimiento puede variar mucho de eso.

    A menos que los usuarios de su clase esperen una @property detrás de cada statement de acceso de atributo, hacer estas cosas explícitas puede ayudar a minimizar las sorpresas de los usuarios de su clase.

  • Como lo mencionó @NeilenMarais y en esta publicación , es más fácil extender los captadores y definidores tradicionales en subclases que extender las propiedades.

  • Los adeptos y setters tradicionales han sido ampliamente utilizados durante mucho tiempo en diferentes idiomas. Si tiene personas de diferentes orígenes en su equipo, se ven más familiares que @property . Además, a medida que su proyecto crezca, si necesita migrar de Python a otro idioma que no tenga @property , el uso de métodos de @property y @property tradicionales facilitará la migración.

Advertencias

  • Ni la @property ni los métodos de @property y @property tradicionales hacen que el miembro de la clase sea privado, incluso si usa el subrayado doble antes de su nombre:

     class Foo: def __init__(self): self.__num = 0 @property def num(self): return self.__num @num.setter def num(self, num): self.__num = num def get_num(self): return self.__num def set_num(self, num): self.__num = num foo = Foo() print(foo.num) # output: 0 print(foo.get_num()) # output: 0 print(foo._Foo__num) # output: 0