web-dev-qa-db-fra.com

Décorateur de propriétés de mémorisation/recherche différée Python

Récemment, j'ai parcouru une base de code existante contenant de nombreuses classes où les attributs d'instance reflètent les valeurs stockées dans une base de données. J'ai refactorisé beaucoup de ces attributs pour que leurs recherches dans la base de données soient différées, c'est-à-dire. ne pas être initialisé dans le constructeur mais uniquement lors de la première lecture. Ces attributs ne changent pas pendant la durée de vie de l'instance, mais constituent un véritable goulot d'étranglement pour le calcul de cette première fois et ne sont réellement utilisés que dans des cas particuliers. Par conséquent, ils peuvent également être mis en cache après avoir été extraits de la base de données (cela correspond donc à la définition de memoisation où l'entrée est simplement "pas d'entrée").

Je me retrouve à taper l'extrait de code suivant maintes et maintes fois pour divers attributs dans différentes classes:

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-t-il déjà un décorateur en Python dont je ne suis tout simplement pas au courant? Ou, existe-t-il un moyen assez simple de définir un décorateur qui fait cela?

Je travaille sous Python 2.5, mais les réponses de la version 2.6 pourraient toujours être intéressantes si elles sont très différentes.

Remarque

Cette question a été posée avant que Python inclue de nombreux décorateurs prêts à l’emploi. Je l'ai mis à jour uniquement pour corriger la terminologie.

102
detly

Pour toutes sortes d’excellents utilitaires, j’utilise boltons .

En tant que partie de cette bibliothèque, vous avez une propriété cachée :

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)
9
guyarad

Voici un exemple d'implémentation d'un décorateur de propriétés paresseux:

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)

Session interactive:

>>> 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]
122
Mike Boers

J'ai écrit celui-ci pour moi-même ... Pour être utilisé pour de vraies propriétés paresseuses calculées une fois. J'aime ça, car cela évite de coller des attributs supplémentaires sur les objets, et une fois activé, ne perd pas de temps à vérifier la présence d'attributs, 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

Remarque: La classe lazy_property est un descripteur non-data , ce qui signifie qu’elle est en lecture seule. Ajouter une méthode __set__ l'empêcherait de fonctionner correctement.

106
Cyclone

Voici un callable qui prend un argument de délai d'expiration facultatif. Dans le __call__, vous pouvez également copier sur le __name__, __doc__, __module__ à partir de l'espace de noms 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
4
gnr

property est une classe. Un descripteur pour être exact. Il suffit d’en dériver et de mettre en œuvre le comportement souhaité.

class lazyproperty(property):
   ....

class testA(object):
   ....
  a = lazyproperty('_a')
  b = lazyproperty('_b')
3

Ce que vous voulez {vraiment} _ c'est la reify (source liée!) decorator de Pyramid:

Utilisez comme un décorateur de méthode de classe. Il fonctionne presque exactement comme le décorateur Python @property, mais il insère le résultat de la méthode qu'il décore dans le dict d'instance après le premier appel, remplaçant ainsi la fonction qu'il décore par une variable d'instance. En langage Python, il s'agit d'un descripteur sans données. Voici un exemple et son utilisation:

>>> 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
2
Antti Haapala

Il existe un mélange de termes et/ou une confusion de concepts à la fois dans la question et dans les réponses.

Une évaluation paresseuse signifie simplement que quelque chose est évalué à l'exécution au dernier moment possible où une valeur est nécessaire. C'est exactement ce que fait le décorateur @property.(*) La fonction décorée est évaluée uniquement et à chaque fois que vous avez besoin de la valeur de cette propriété. (voir l'article de Wikipédia sur l'évaluation paresseuse)

(*) En réalité, une évaluation vraiment paresseuse (comparer, par exemple, haskell) est très difficile à réaliser en python (et aboutit à un code qui est loin d’être idiomatique). 

La mémorisation est le terme correct pour ce que le demandeur semble rechercher. Les fonctions pures qui ne dépendent pas des effets secondaires pour l’évaluation de la valeur de retour peuvent être mémorisées en toute sécurité. Il existe en fait un décorateur dans functools@functools.lru_cache, vous n’avez donc pas besoin d’écrire vous-même, sauf si vous avez besoin d’un comportement spécialisé.

0
Jason Herbburn

Vous pouvez le faire Nice et facilement en construisant une classe à partir de la propriété native Python:

class cached_property(property):
    def __init__(self, func, name=None, doc=None):
        self.__= 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

Nous pouvons utiliser cette classe de propriété comme une propriété de classe normale

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)

Valeur uniquement calculée pour la première fois et ensuite utilisation de la valeur enregistrée

Sortie:

I am calculating value
My calculated value
My calculated value
2
2
0
itmard