[ TL; DR? Puedes saltar al final para ver un ejemplo de código .]
De hecho, prefiero usar un idioma diferente, que es un poco complicado de usar como algo único, 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 permiten acceder a los atributos como atributos. Podemos convertir 'gets' en 'cálculos' (esencialmente) y podemos convertir 'sets' en 'eventos'. 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()
Tal vez se pregunte por qué no llamé defaultX
y defaultY
en el __init__
método del objeto . La razón es que para nuestro caso quiero suponer que los someDefaultComputation
métodos devuelven valores que varían con el tiempo, digamos una marca de tiempo, y siempre que x
(o y
) no esté establecido (donde, para el propósito de este ejemplo, "no establecido" significa "establecido" a Ninguno ") Quiero el valor del cálculo predeterminado de x
'(o y
' s).
Así que esto es lamentable 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 ganado la capacidad de referirnos a estos atributos como atributos aunque, 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 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 cada vez que no se establece el valor. Este es un patrón muy común.
Pero, ¿qué estamos perdiendo y qué no podemos hacer?
La molestia principal, en mi opinión, es que si define un captador (como lo hacemos aquí) también debe definir un setter. [1] Eso es ruido extra que satura el código.
Otra molestia es que todavía tenemos que inicializar los valores x
y . (Bueno, por supuesto, podríamos agregarlos usando, pero eso es más código adicional).y
__init__
setattr()
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 cerca que podemos llegar es anular la asignación para implicar una semántica especial:
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 así.
Pero incluso si lo hiciéramos, aún no podríamos admitirlo con propiedades porque no hay forma de obtener el valor porque no podemos pasar parámetros en absoluto 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 / setter.
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 fallan en 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 el __init__
o setattr
en el código. La variable simplemente existe por el hecho de que hemos creado este nuevo atributo de 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 luego poder 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 manera de hacer esto para el caso especial de un atributo parametrizable, pero aún así permitimos que funcione el caso de asignación predeterminado. Verá cómo abordé esto a continuación.
Ahora al punto (¡sí! ¡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 un conjunto de variables, pero también mantiene un identificador de código que sabe cómo calcular un valor predeterminado. Su trabajo es almacenar el conjunto value
o ejecutar method
si ese valor no está establecido.
Llamémoslo un 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 method
aquí hay un método de clase, value
es el valor de UberProperty
, y he agregado isSet
porque None
puede ser un valor real y esto nos permite una forma clara de declarar que realmente no hay "ningún 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 realmente en nuestra clase? Bueno, las propiedades usan decoradores; porque no podemos Veamos cómo podría verse (de aquí en adelante me limitaré a usar solo un 'atributo' x
).
class Example(object):
@uberProperty
def x(self):
return defaultX()
En realidad, esto aún no funciona, por supuesto. Tenemos que implementar uberProperty
y asegurarnos de que maneja tanto get como sets.
Comencemos con gets.
Mi primer intento fue simplemente crear un nuevo objeto UberProperty y devolverlo:
def uberProperty(f):
return UberProperty(f)
Rápidamente descubrí, por supuesto, que esto no funciona: Python nunca une lo invocable al objeto y necesito el objeto para llamar a la función. Incluso crear el decorador en la clase no funciona, ya que aunque ahora tenemos la clase, todavía no tenemos un objeto con el que trabajar.
Entonces vamos a necesitar poder hacer más aquí. Sabemos que un método solo necesita ser representado una vez, así que sigamos con nuestro decorador, pero modifiquemos UberProperty
para almacenar solo la method
referencia:
class UberProperty(object):
def __init__(self, method):
self.method = method
Tampoco es invocable, por lo que por el momento nada funciona.
¿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 lo UberProperty
que, por supuesto, no es invocable, por lo que esto no es de mucha utilidad.
Lo que necesitamos es alguna forma de vincular dinámicamente la 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 se haya devuelto a ese usuario para su uso. Um, sí, es una __init__
llamada, amigo.
Escribamos lo que queremos que nuestro resultado sea primero. Estamos vinculando UberProperty
a una instancia, por lo que una cosa obvia para devolver sería una BoundUberProperty. Aquí es donde realmente mantendremos el estado del 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 llevar esto a un objeto? Hay algunos enfoques, pero el más fácil de explicar solo usa el __init__
método para hacer ese mapeo. En el momento en que __init__
se llama, nuestros decoradores se han ejecutado, por lo que solo debemos mirar a través de los objetos __dict__
y actualizar los atributos donde el valor del atributo es de tipo UberProperty
.
Ahora, las propiedades súper 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á 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)
Agregamos 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 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
(Gee, estoy trabajando hasta tarde)
Tenga en cuenta que he utilizado getValue
, setValue
y clearValue
aquí. Esto se debe a que aún no he vinculado los medios para devolverlos 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 escaparatismo. Escaparate de usabilidad importante, 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 UberObject's __init__
siempre sea llamado por subclases.
- Por lo tanto, forzamos que se llame a alguna parte o evitamos que se implemente.
- Veremos cómo hacer esto con una metaclase.
Necesitamos asegurarnos de manejar el caso común en el que alguien 'alias' una función a 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 tendremos que usar una llamada de función para obtener el valor.
- Pero podemos hacer que parezca una llamada de función normal y evitar tener que usarla
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 padre, pero necesitaremos actualizar nuestro __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 sobre si este sigue siendo el caso.