property - getters y setters en python
Usando @property contra getters y setters (12)
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.
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 .
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, existe una gran cantidad de código con la extensión .py que utiliza captadores y definidores y clases de herencia e inútiles en todas partes donde, por ejemplo, una tupla simple lo haría, pero es código de personas que escriben en C ++ o Java utilizando Python.
Eso no es el código de Python.
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.
Me sorprende que nadie haya mencionado que las propiedades son métodos limitados de una clase de descriptores, y entienden exactamente esta idea en sus publicaciones, que los y 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 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 esclavitud y disciplina (es decir, Java), les falta el punto de descriptores.
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.
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 tipoFoo
, que tiene un número de miembro. Entonces simplemente puede obtener este miembro connum = foo.num
. A medida que su proyecto crece, puede sentir que debe haber algunas comprobaciones o errores en el acceso a atributos simples. Entonces 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 / mutador. Tenga en cuenta que Python proporciona un camino fácil para futuras mejoras, en caso de que necesite un atributo de datos simple para crecer 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 simples.
El uso de
@property
para el acceso a datos en Python se considera Pythonic :Puede fortalecer su autoidentificación como programador 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-patterns .
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 aumentar un acceso de miembro directo como
foo.num = num
, puede aumentar fácilmente su configurador tradicional con un parámetro deforce
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 declaración de acceso de atributo, hacer que estas cosas sean explícitas puede ayudar a minimizar las sorpresas de los usuarios de su clase.Como lo mencionó share y en esta publicación , es más fácil extender los share y share 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
@property
ni los getters y setters tradicionales hacen que el miembro de la clase sea privado, incluso si usa un 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
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__
osetattr
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 devolverexgetValue()
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 enex = <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.
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.