Decorador de propiedades de búsqueda diferida / memorización de Python

109

Recientemente, revisé una base de código existente que contiene muchas clases donde los atributos de instancia reflejan valores almacenados en una base de datos. He refactorizado muchos de estos atributos para que sus búsquedas en la base de datos se difieran, es decir. no debe inicializarse en el constructor sino solo en la primera lectura. Estos atributos no cambian durante la vida útil de la instancia, pero son un verdadero cuello de botella para calcular esa primera vez y solo se accede a ellos realmente para casos especiales. Por lo tanto, también se pueden almacenar en caché después de que se hayan recuperado de la base de datos (esto, por lo tanto, se ajusta a la definición de memorización donde la entrada es simplemente "sin entrada").

Me encuentro escribiendo el siguiente fragmento de código una y otra vez para varios atributos en varias clases:

class testA(object):

  def __init__(self):
    self._a = None
    self._b = None

  @property
  def a(self):
    if self._a is None:
      # Calculate the attribute now
      self._a = 7
    return self._a

  @property
  def b(self):
    #etc

¿Existe un decorador existente para hacer esto en Python que simplemente no conozco? ¿O hay una forma razonablemente sencilla de definir un decorador que haga esto?

Estoy trabajando con Python 2.5, pero las respuestas 2.6 aún pueden ser interesantes si son significativamente diferentes.

Nota

Esta pregunta se hizo antes de que Python incluyera muchos decoradores preparados para esto. Lo he actualizado solo para corregir la terminología.

detenidamente
fuente
Estoy usando Python 2.7 y no veo nada sobre decoradores listos para usar para esto. ¿Puede proporcionar un enlace a los decoradores listos para usar que se mencionan en la pregunta?
Bamcclur
@Bamcclur lo siento, solía haber otros comentarios que los detallaban, no estoy seguro de por qué se eliminaron. El único que puedo encontrar en este momento es un Python 3 uno: functools.lru_cache().
detly
No estoy seguro de que haya elementos integrados (al menos Python 2.7), pero está la propiedad
guyarad
@guyarad No vi este comentario hasta ahora. ¡Es una biblioteca fantástica! Publique eso como respuesta para que pueda votar a favor.
detly

Respuestas:

12

Para todo tipo de grandes utilidades, estoy usando boltons .

Como parte de esa biblioteca, tiene la propiedad en caché :

from boltons.cacheutils import cachedproperty

class Foo(object):
    def __init__(self):
        self.value = 4

    @cachedproperty
    def cached_prop(self):
        self.value += 1
        return self.value


f = Foo()
print(f.value)  # initial value
print(f.cached_prop)  # cached property is calculated
f.value = 1
print(f.cached_prop)  # same value for the cached property - it isn't calculated again
print(f.value)  # the backing value is different (it's essentially unrelated value)
guyarad
fuente
124

Aquí hay una implementación de ejemplo de un decorador de propiedades perezoso:

import functools

def lazyprop(fn):
    attr_name = '_lazy_' + fn.__name__

    @property
    @functools.wraps(fn)
    def _lazyprop(self):
        if not hasattr(self, attr_name):
            setattr(self, attr_name, fn(self))
        return getattr(self, attr_name)

    return _lazyprop


class Test(object):

    @lazyprop
    def a(self):
        print 'generating "a"'
        return range(5)

Sesión interactiva:

>>> t = Test()
>>> t.__dict__
{}
>>> t.a
generating "a"
[0, 1, 2, 3, 4]
>>> t.__dict__
{'_lazy_a': [0, 1, 2, 3, 4]}
>>> t.a
[0, 1, 2, 3, 4]
Mike Boers
fuente
1
¿Alguien puede recomendar un nombre apropiado para la función interna? Soy tan malo nombrando cosas en la mañana ...
Mike Boers
2
Por lo general, nombro la función interna de la misma manera que la función externa con un guión bajo anterior. Entonces "_lazyprop" - sigue la filosofía de "solo uso interno" de pep 8.
spenthil
1
Esto funciona muy bien :) No sé por qué nunca se me ocurrió usar un decorador en una función anidada como esa también.
especial el
4
dado el protocolo sin descriptor de datos, este es mucho más lento y menos elegante que la respuesta a continuación usando__get__
Ronny
1
Consejo: Ponga a @wraps(fn)continuación @propertypara no perder sus cadenas de documentos, etc. ( wrapsviene de functools)
letmaik
111

Escribí este para mí ... Para usarlo en verdaderas propiedades perezosas calculadas una sola vez . Me gusta porque evita pegar atributos extra en los objetos, y una vez activado no pierde tiempo comprobando la presencia de atributos, etc .:

import functools

class lazy_property(object):
    '''
    meant to be used for lazy evaluation of an object attribute.
    property should represent non-mutable data, as it replaces itself.
    '''

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

        # copy the getter function's docstring and other attributes
        functools.update_wrapper(self, fget)

    def __get__(self, obj, cls):
        if obj is None:
            return self

        value = self.fget(obj)
        setattr(obj, self.fget.__name__, value)
        return value


class Test(object):

    @lazy_property
    def results(self):
        calcs = 1  # Do a lot of calculation here
        return calcs

Nota: La lazy_propertyclase es un descriptor que no es de datos , lo que significa que es de solo lectura. Agregar un __set__método evitaría que funcione correctamente.

Ciclón
fuente
9
Esto tardó un poco en comprender, pero es una respuesta absolutamente sorprendente. Me gusta cómo la función en sí se reemplaza por el valor que calcula.
Paul Etherton
2
Para la posteridad: se han propuesto otras versiones de esto en otras respuestas desde (ref 1 y 2 ). Parece que este es uno de los populares en los marcos web de Python (existen derivados en Pyramid y Werkzeug).
André Caron
1
Gracias por señalar que Werkzeug tiene werkzeug.utils.cached_property: werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property
divieira
3
Encontré que este método es 7.6 veces más rápido que la respuesta seleccionada. (2,45 µs / 322 ns) Ver cuaderno de ipython
Dave Butler
1
NB: esto no impide la asignación a fgetla forma @property. Para garantizar la inmutabilidad / idempotencia, debe agregar un __set__()método que aumente AttributeError('can\'t set attribute')(o cualquier excepción / mensaje que le convenga, pero esto es lo que propertysurge). Desafortunadamente, esto tiene un impacto en el rendimiento de una fracción de microsegundo porque __get__()se llamará en cada acceso en lugar de extraer el valor de fget de dict en el segundo acceso y en los siguientes. En mi opinión, vale la pena mantener la inmutabilidad / idempotencia, que es clave para mis casos de uso, pero YMMV.
scanny
4

He aquí una invocable que toma un argumento opcional de tiempo de espera, en el __call__que también podría copiar el __name__, __doc__, __module__del espacio de nombres de func:

import time

class Lazyproperty(object):

    def __init__(self, timeout=None):
        self.timeout = timeout
        self._cache = {}

    def __call__(self, func):
        self.func = func
        return self

    def __get__(self, obj, objcls):
        if obj not in self._cache or \
          (self.timeout and time.time() - self._cache[key][1] > self.timeout):
            self._cache[obj] = (self.func(obj), time.time())
        return self._cache[obj]

ex:

class Foo(object):

    @Lazyproperty(10)
    def bar(self):
        print('calculating')
        return 'bar'

>>> x = Foo()
>>> print(x.bar)
calculating
bar
>>> print(x.bar)
bar
...(waiting 10 seconds)...
>>> print(x.bar)
calculating
bar
gnr
fuente
3

propertyes una clase. Un descriptor para ser exactos. Simplemente deriva de él e implementa el comportamiento deseado.

class lazyproperty(property):
   ....

class testA(object):
   ....
  a = lazyproperty('_a')
  b = lazyproperty('_b')
Ignacio Vázquez-Abrams
fuente
3

Lo que realmente quieres es el decorador reify(fuente vinculada) de Pyramid:

Úselo como decorador de métodos de clase. Opera casi exactamente como el @propertydecorador de Python , pero coloca el resultado del método que decora en el dictado de instancia después de la primera llamada, reemplazando efectivamente la función que decora con una variable de instancia. Es, en el lenguaje de Python, un descriptor que no es de datos. El siguiente es un ejemplo y su uso:

>>> from pyramid.decorator import reify

>>> class Foo(object):
...     @reify
...     def jammy(self):
...         print('jammy called')
...         return 1

>>> f = Foo()
>>> v = f.jammy
jammy called
>>> print(v)
1
>>> f.jammy
1
>>> # jammy func not called the second time; it replaced itself with 1
>>> # Note: reassignment is possible
>>> f.jammy = 2
>>> f.jammy
2
Antti Haapala
fuente
1
Bonito, hace exactamente lo que necesitaba ... aunque Pyramid podría ser una gran dependencia para un decorador:)
especial
@detly La implementación del decorador es simple y podría implementarla usted mismo, sin necesidad de la pyramiddependencia.
Peter Wood
Por lo tanto, el enlace dice "fuente vinculada": D
Antti Haapala
@AnttiHaapala Lo noté, pero pensé en resaltar que es simple de implementar para aquellos que no siguen el enlace.
Peter Wood
1

Hay una mezcla de términos y / o confusión de conceptos tanto en la pregunta como en las respuestas hasta ahora.

La evaluación diferida solo significa que algo se evalúa en tiempo de ejecución en el último momento posible cuando se necesita un valor. El @propertydecorador estándar hace precisamente eso. (*) La función decorada se evalúa solo y cada vez que se necesita el valor de esa propiedad. (consulte el artículo de wikipedia sobre evaluación perezosa)

(*) En realidad, una verdadera evaluación perezosa (compárese, por ejemplo, con haskell) es muy difícil de lograr en Python (y da como resultado un código que está lejos de ser idiomático).

La memorización es el término correcto para lo que parece estar buscando el solicitante. Las funciones puras que no dependen de los efectos secundarios para la evaluación del valor de retorno se pueden memorizar de forma segura y, de hecho, hay un decorador en funciones, @functools.lru_cache por lo que no es necesario escribir decoradores propios a menos que necesite un comportamiento especializado.

Jason Herbburn
fuente
Utilicé el término "perezoso" porque en la implementación original, el miembro se calculó / recuperó de una base de datos en el momento de la inicialización del objeto, y quiero aplazar ese cálculo hasta que la propiedad se use realmente en una plantilla. Esto me pareció coincidir con la definición de pereza. Estoy de acuerdo en que, dado que mi pregunta ya supone una solución @property, "lazy" no tiene mucho sentido en ese momento. (También pensé en memoisation como un mapa de entradas a salidas en caché, y puesto que estas propiedades tienen una sola entrada, nada, un mapa que parecía más la complejidad de lo necesario.)
detly
Tenga en cuenta que todos los decoradores que la gente ha sugerido como soluciones "listas para usar" tampoco existían cuando pregunté esto.
detly
Estoy de acuerdo con Jason, esta es una pregunta sobre almacenamiento en caché / memorización, no evaluación perezosa.
poindexter
@poindexter - El almacenamiento en caché no todo lo cubre; no distingue buscar el valor en el momento de inicio del objeto y almacenarlo en caché de buscar el valor y almacenarlo en caché cuando se accede a la propiedad (que es la característica clave aquí). ¿Cómo debo llamarlo? ¿Decorador de "caché después del primer uso"?
detly
@detly Memoize. Deberías llamarlo Memoize. en.wikipedia.org/wiki/Memoization
poindexter
0

Puede hacer esto de forma agradable y sencilla creando una clase a partir de la propiedad nativa de Python:

class cached_property(property):
    def __init__(self, func, name=None, doc=None):
        self.__name__ = name or func.__name__
        self.__module__ = func.__module__
        self.__doc__ = doc or func.__doc__
        self.func = func

    def __set__(self, obj, value):
        obj.__dict__[self.__name__] = value

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        value = obj.__dict__.get(self.__name__, None)
        if value is None:
            value = self.func(obj)
            obj.__dict__[self.__name__] = value
        return value

Podemos usar esta clase de propiedad como propiedad de clase normal (también es compatible con la asignación de elementos como puede ver)

class SampleClass():
    @cached_property
    def cached_property(self):
        print('I am calculating value')
        return 'My calculated value'


c = SampleClass()
print(c.cached_property)
print(c.cached_property)
c.cached_property = 2
print(c.cached_property)
print(c.cached_property)

Valor solo calculado la primera vez y después de eso usamos nuestro valor guardado

Salida:

I am calculating value
My calculated value
My calculated value
2
2
rezakamalifard
fuente