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


109

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 remanié 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 au cours de la durée de vie de l'instance, mais ils constituent un véritable goulot d'étranglement pour calculer cette première fois et ne sont vraiment accessibles que pour des cas particuliers. Par conséquent, ils peuvent également être mis en cache après avoir été récupérés de la base de données (cela correspond donc à la définition de la mémoisation où l'entrée est simplement "aucune entrée").

Je me surprends à taper encore et encore l'extrait de code suivant 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 pour faire cela en Python dont je ne suis tout simplement pas au courant? Ou y a-t-il un moyen raisonnablement simple de définir un décorateur qui fait cela?

Je travaille sous Python 2.5, mais les réponses 2.6 peuvent être intéressantes si elles sont significativement différentes.

Remarque

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


J'utilise Python 2.7 et je ne vois rien sur les décorateurs prêts à l'emploi pour cela. Pouvez-vous fournir un lien vers les décorateurs prêts à l'emploi mentionnés dans la question?
Bamcclur

@Bamcclur désolé, il y avait autrefois d'autres commentaires les détaillant, je ne sais pas pourquoi ils ont été supprimés. Le seul que je peux trouver est en ce moment un Python 3 un: functools.lru_cache().
detly

Je ne suis pas sûr qu'il y ait des éléments intégrés (au moins Python 2.7), mais il y a la propriété cachée de la bibliothèque Boltons
guyarad

@guyarad Je n'ai pas vu ce commentaire jusqu'à présent. C'est une bibliothèque fantastique! Postez cela comme une réponse afin que je puisse voter pour.
detly le

Réponses:


12

Pour toutes sortes de grands utilitaires, j'utilise des boulons .

Dans le cadre de cette bibliothèque, vous avez mis en cache la propriété :

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)

124

Voici un exemple d'implémentation d'un décorateur de propriété 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]

1
Quelqu'un peut-il recommander un nom approprié pour la fonction intérieure? Je suis si mauvais pour nommer les choses le matin ...
Mike Boers

2
Je nomme généralement la fonction interne de la même manière que la fonction externe avec un trait de soulignement précédent. Donc "_lazyprop" - suit la philosophie "usage interne uniquement" de pep 8.
passé le

1
Cela fonctionne très bien :) Je ne sais pas pourquoi il ne m'est jamais venu à l'esprit d'utiliser un décorateur sur une fonction imbriquée comme celle-là.
detly le

4
étant donné le protocole de descripteur de non-données, celui-ci est beaucoup plus lent et moins élégant que la réponse ci-dessous en utilisant__get__
Ronny

1
Astuce: mettez un @wraps(fn)ci - dessous @propertypour ne pas perdre vos chaînes de documents, etc. ( wrapsvient de functools)
letmaik

111

J'ai écrit celui-ci pour moi-même ... A utiliser pour de vraies propriétés paresseuses calculées une seule fois . Je l'aime 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 lazy_propertyclasse est un descripteur sans données , ce qui signifie qu'elle est en lecture seule. L'ajout d'une __set__méthode l'empêcherait de fonctionner correctement.


9
Cela a pris un peu de temps à comprendre mais c'est une réponse absolument stupéfiante. J'aime la façon dont la fonction elle-même est remplacée par la valeur qu'elle calcule.
Paul Etherton

2
Pour la postérité: d'autres versions de ceci ont été proposées dans d'autres réponses depuis (réf. 1 et 2 ). Il semble que ce soit un modèle populaire dans les frameworks Web Python (des dérivés existent dans Pyramid et Werkzeug).
André Caron

1
Merci de noter que Werkzeug a werkzeug.utils.cached_property: werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property
divieira

3
J'ai trouvé cette méthode 7,6 fois plus rapide que la réponse sélectionnée. (2,45 µs / 322 ns) Voir le cahier ipython
Dave Butler

1
NB: cela n'empêche pas l'affectation à fgetla voie @property. Pour garantir l'immuabilité / idempotence, vous devez ajouter une __set__()méthode qui déclenche AttributeError('can\'t set attribute')(ou toute exception / message qui vous convient, mais c'est ce qui propertydéclenche). Cela vient malheureusement avec un impact sur les performances d'une fraction de microseconde car __get__()sera appelé à chaque accès plutôt que d'extraire la valeur fget de dict lors du deuxième accès et des accès suivants. Cela vaut la peine à mon avis de maintenir l'immuabilité / l'idempotence, ce qui est essentiel pour mes cas d'utilisation, mais YMMV.
scanny le

4

Voici une appelable qui prend un argument de délai d' attente en option, dans la __call__vous pouvez aussi copier le __name__, __doc__, __module__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

3

propertyest une classe. Un descripteur pour être exact. Dérivez-en simplement et implémentez le comportement souhaité.

class lazyproperty(property):
   ....

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

3

Ce que vous voulez vraiment, c'est le décorateur reify(lié à la source!) De Pyramid:

À utiliser comme décorateur de méthode de classe. Il fonctionne presque exactement comme le @propertydécorateur Python , mais il place le résultat de la méthode qu'il décore dans l'instance dict après le premier appel, remplaçant effectivement la fonction qu'il décore par une variable d'instance. C'est, dans le langage Python, 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

1
Belle, fait exactement ce dont j'avais besoin ... bien que Pyramid puisse être une grande dépendance pour un décorateur:)
detly

@detly L'implémentation du décorateur est simple, et vous pouvez l'implémenter vous-même, pas besoin de pyramiddépendance.
Peter Wood

D'où le lien dit "source liée": D
Antti Haapala

@AnttiHaapala J'ai remarqué, mais j'ai pensé souligner que c'est simple à mettre en œuvre pour ceux qui ne suivent pas le lien.
Peter Wood

1

Il y a un mélange de termes et / ou une confusion des concepts à la fois en question et dans les réponses jusqu'à présent.

L'évaluation paresseuse signifie simplement que quelque chose est évalué au moment de l'exécution au dernier moment possible lorsqu'une valeur est nécessaire. C'est @propertyexactement ce que fait le décorateur standard . (*) 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 wikipedia sur l'évaluation paresseuse)

(*) En fait, une véritable évaluation 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é et il y a en fait un décorateur dans les fonctools @functools.lru_cache donc pas besoin d'écrire ses propres décorateurs sauf si vous avez besoin d'un comportement spécialisé.


J'ai utilisé le terme "paresseux" parce que dans l'implémentation d'origine, le membre a été calculé / récupéré à partir d'une base de données au moment de l'initialisation de l'objet, et je souhaite reporter ce calcul jusqu'à ce que la propriété soit réellement utilisée dans un modèle. Cela me semblait correspondre à la définition de la paresse. Je suis d'accord que puisque ma question suppose déjà une solution utilisant @property, "paresseux" n'a pas beaucoup de sens à ce stade. (J'ai également pensé à la mémoisation comme une carte des entrées aux sorties mises en cache, et comme ces propriétés n'ont qu'une seule entrée, rien, une carte semblait plus complexe que nécessaire.)
detly

Notez que tous les décorateurs que les gens ont suggérés comme des solutions «prêtes à l'emploi» n'existaient pas non plus lorsque j'ai posé cette question.
detly le

Je suis d'accord avec Jason, c'est une question sur la mise en cache / mémorisation et non sur l'évaluation paresseuse.
poindexter

@poindexter - La mise en cache ne le couvre pas tout à fait; il ne distingue pas la recherche de la valeur au moment de l'initialisation de l'objet et sa mise en cache de la recherche de la valeur et de sa mise en cache lors de l'accès à la propriété (ce qui est la caractéristique clé ici). Comment dois-je l'appeler? Décorateur "Cache après la première utilisation"?
detly

@detly Memoize. Vous devriez l'appeler Memoize. en.wikipedia.org/wiki/Memoization
poindexter

0

Vous pouvez faire cela facilement 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__ = 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 régulière (elle prend également en charge l'attribution d'éléments comme vous pouvez le voir)

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 calculée uniquement la première fois et après cela, nous avons utilisé notre valeur enregistrée

Production:

I am calculating value
My calculated value
My calculated value
2
2
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.