110 votes

Python memoising/deferred lookup property decorator

Récemment, j'ai parcouru une base de code existante contenant de nombreuses classes dont les attributs d'instance reflètent des valeurs stockées dans une base de données. J'ai remanié un grand nombre de ces attributs pour que la consultation de la base de données soit différée, c'est-à-dire qu'elle ne soit pas initialisée dans le constructeur mais seulement à la première lecture. Ces attributs ne changent pas pendant la durée de vie de l'instance, mais ils sont un vrai goulot d'étranglement pour être calculés la première fois et ne sont vraiment utilisés que dans des cas particuliers. Par conséquent, ils peuvent également être mis en cache après avoir été récupérés dans la base de données (ce qui correspond donc à la définition de l'expression mémorisation où l'entrée est simplement "aucune entrée").

Je me retrouve à taper le bout de code suivant encore et encore pour divers attributs dans diverses 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 un décorateur pour faire cela en Python dont je ne suis pas au courant ? Ou bien, existe-t-il un moyen raisonnablement simple de définir un décorateur qui fasse cela ?

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

Note

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

0 votes

J'utilise Python 2.7, et je ne vois rien concernant des 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 ?

0 votes

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

0 votes

Je ne suis pas sûr qu'il y ait des modules intégrés (au moins pour Python 2.7), mais il y a la bibliothèque Boltons avec propriété en cachette

125voto

Mike Boers Points 3999

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]

1 votes

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

2 votes

J'ai l'habitude de nommer la fonction interne de la même manière que la fonction externe, en la précédant d'un trait de soulignement. Donc "_lazyprop" - suit la philosophie "usage interne seulement" de pep 8.

1 votes

Cela fonctionne très bien :) Je ne sais pas pourquoi je n'ai jamais pensé à utiliser un décorateur sur une fonction imbriquée comme celle-ci.

111voto

Cyclone Points 830

J'ai écrit celui-ci pour moi-même... Pour être utilisé pour vrai unique propriétés paresseuses calculées. Je l'aime parce qu'il é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

Note : Le lazy_property est une classe descripteur de non-données ce qui signifie qu'il est en lecture seule. L'ajout d'un __set__ l'empêcherait de fonctionner correctement.

9 votes

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.

2 votes

Pour la postérité : d'autres versions de ce principe ont été proposées dans d'autres réponses depuis (réf. 1 et 2 ). Il semble qu'il s'agisse d'un élément populaire dans les frameworks web Python (des dérivés existent dans Pyramid et Werkzeug).

1 votes

Merci de noter que Werkzeug dispose de werkzeug.utils.cached_property : outil.pocoo.org/docs/utils/#outil.utils.cached_property

13voto

Guy Points 81

Pour toutes sortes de grands utilitaires, j'utilise boltons .

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

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)

4voto

gnr Points 682

Voici un appelable qui prend un argument de délai optionnel, dans le fichier __call__ vous pouvez également 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

3voto

property est une classe. A descripteur pour être exact. Il suffit d'en dériver et d'implémenter le comportement souhaité.

class lazyproperty(property):
   ....

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X