2010-10-27 10 views
31

estoy escribiendo una clase en Python y tengo un atributo que va a tomar un tiempo relativamente largo para calcular, por lo sólo quiero hacerlo una vez. Además, no será necesaria por cada instancia de la clase, por lo no quiero hacerlo de manera predeterminada en__init__.atributos "Caching" de clases en Python

Soy nuevo en Python, pero no a la programación. Puedo llegar a una manera de hacer esto con bastante facilidad, pero he encontrado una y otra vez que la forma 'Pythonic' de hacer algo es a menudo mucho más simple que lo que me ocurre con el uso de mi experiencia en otros idiomas.

¿Hay una manera 'correcta' de hacer esto en Python?

+2

IMO ninguna de estas respuestas es correcta. OP quería una propiedad _class_ en caché, por ejemplo 'Foo.something_expensive'. Todas estas respuestas son sobre las _instance_ propiedades almacenadas en caché, lo que significa que 'something_expensive' se volverá a calcular para cada instancia nueva, que es menos que óptima en la mayoría de los casos – steve

Respuesta

26

La forma más habitual sería hacer que el atributo de un property y almacenar el valor de la primera vez que se calcula

import time 

class Foo(object): 
    def __init__(self): 
     self._bar = None 

    @property 
    def bar(self): 
     if self._bar is None: 
      print "starting long calculation" 
      time.sleep(5) 
      self._bar = 2*2 
      print "finished long caclulation" 
     return self._bar 

foo=Foo() 
print "Accessing foo.bar" 
print foo.bar 
print "Accessing foo.bar" 
print foo.bar 
+0

En Python3.2 +, ¿hay alguna motivación para usar este enfoque sobre' @property + @ functools.lru_cache() '? El modo de atributo casi privado parece ser una reminiscencia de Java/setters/getters; en mi humilde opinión, simplemente decorar con lru_cache es más pythonic –

+0

(Como en @ Maxime [respuesta] (https://stackoverflow.com/a/19979379/7954504)) –

-2

La forma más sencilla de hacer esto, probablemente sería simplemente escribir un método (en lugar de utilizar un atributo) que se envuelve alrededor del atributo (método de obtención). En la primera llamada, este método calcula, guarda y devuelve el valor; más tarde simplemente devuelve el valor guardado.

1

Usted podría intentar buscar en memoization. La forma en que funciona es que si pasas en una función los mismos argumentos, devolverá el resultado en caché. Puede encontrar más información en implementing it in python here.

También, dependiendo de cómo el código está configurado (se dice que no es necesario para todos los casos) se podría tratar de utilizar algún tipo de patrón de peso mosca, o perezoso de carga.

2
class MemoizeTest: 

     _cache = {} 
     def __init__(self, a): 
      if a in MemoizeTest._cache: 
       self.a = MemoizeTest._cache[a] 
      else: 
       self.a = a**5000 
       MemoizeTest._cache.update({a:self.a}) 
36

Solía ​​hacer esto como sugirió gnibbler, pero finalmente me cansé de los pequeños pasos de la limpieza.

Así que construí mi propio descriptor:

class cached_property(object): 
    """ 
    Descriptor (non-data) for building an attribute on-demand on first use. 
    """ 
    def __init__(self, factory): 
     """ 
     <factory> is called such: factory(instance) to build the attribute. 
     """ 
     self._attr_name = factory.__name__ 
     self._factory = factory 

    def __get__(self, instance, owner): 
     # Build the attribute. 
     attr = self._factory(instance) 

     # Cache the value; hide ourselves. 
     setattr(instance, self._attr_name, attr) 

     return attr 

Así es como debería usar este recurso:

class Spam(object): 

    @cached_property 
    def eggs(self): 
     print 'long calculation here' 
     return 6*2 

s = Spam() 
s.eggs  # Calculates the value. 
s.eggs  # Uses cached value. 
+7

¡Maravilloso! Así es como funciona: las variables de instancia [tienen prioridad sobre los descriptores que no son de datos] (https://docs.python.org/2/howto/descriptor.html#descriptor-protocol). En el primer acceso del atributo, no hay ningún atributo de instancia sino solo el atributo de clase de descriptor y, por lo tanto, se ejecuta el descriptor. Sin embargo, durante su ejecución, el descriptor crea un atributo de instancia con el valor en caché. Esto significa que cuando se accede al atributo por segunda vez, se devuelve el atributo de instancia creado previamente en lugar de que se ejecute el descriptor. –

+1

Hay un paquete ['cached_property' en PyPI] (https://pypi.python.org/pypi/cached-property). Incluye versiones seguras y de tiempo de ejecución de subprocesos. (También, gracias, @Florian, por la explicación). – leewz

+2

Yay para casos esotéricos en las esquinas: no se puede usar un descriptor 'cached_property' cuando se usa' __slots__'. Las ranuras se implementan utilizando descriptores de datos, y el uso del descriptor 'cached_property' simplemente anula el descriptor de ranura generado, por lo que la llamada' setattr() 'no funcionará ya que no hay' __dict__' para establecer el atributo y el único descriptor disponible para este nombre de atributo es 'cached_property' .. Simplemente poniendo esto aquí para ayudar a otros a evitar este error. –

21
  • Python> = 3,2

Debe utilizar decoradores @property y @functools.lru_cache:

import functools 
class MyClass: 
    @property 
    @functools.lru_cache() 
    def foo(self): 
     print("long calculation here") 
     return 21 * 2 

This answer tiene ejemplos más detallados y también menciona un backport para las versiones de Python anteriores.

  • Python < 3,2

El pitón wiki tiene un cached property decorator (MIT licencia) que puede ser utilizado como esto:

import random 
# the class containing the property must be a new-style class 
class MyClass(object): 
    # create property whose value is cached for ten minutes 
    @cached_property(ttl=600) 
    def randint(self): 
     # will only be evaluated every 10 min. at maximum. 
     return random.randint(0, 100) 

O cualquier aplicación mencionado en las otras respuestas que se adapta a tus necesidades
O el backport mencionado anteriormente.

+3

lru_cache también se ha backported a python 2: https://pypi.python.org/pypi/functools32/3.2.3 – Buttons840

+0

-1 'lru_cache' tiene un tamaño predeterminado de 128, hace que la función de propiedad sea potencialmente llamada dos veces. Si tuviera que usar 'lru_cache (None)', todas las instancias se mantendrán activas de forma permanente. – orlp

+2

@orlp lru_cache tiene un tamaño predeterminado de 128, para 128 configuraciones de argumentos diferentes. Esto solo será un problema si está generando más objetos que su tamaño de caché, ya que el único argumento cambiante aquí es el propio. Si está generando tantos objetos, realmente no debería usar un caché sin límites, ya que le obligará a mantener indefinidamente todos los objetos que alguna vez llamaron a la propiedad, lo que podría ser una terrible pérdida de memoria. De todos modos, probablemente sería mejor con un método de almacenamiento en caché que almacena la caché en el objeto en sí, por lo que la caché se limpia con ella. – Taywee