Existe-t-il un décorateur pour simplement mettre en cache les valeurs de retour de fonction?

157

Considérer ce qui suit:

@property
def name(self):

    if not hasattr(self, '_name'):

        # expensive calculation
        self._name = 1 + 1

    return self._name

Je suis nouveau, mais je pense que la mise en cache pourrait être prise en compte dans un décorateur. Seulement je n'en ai pas trouvé un comme ça;)

PS le calcul réel ne dépend pas des valeurs mutables

Tobias
la source
Il y a peut-être un décorateur qui a une capacité comme celle-là, mais vous n'avez pas complètement spécifié ce que vous voulez. Quel type de backend de mise en cache utilisez-vous? Et comment la valeur sera-t-elle saisie? Je suppose d'après votre code que ce que vous demandez vraiment est une propriété en lecture seule mise en cache.
David Berger
Il existe des décorateurs de mémorisation qui effectuent ce que vous appelez la «mise en cache»; ils travaillent généralement sur des fonctions en tant que telles (qu'elles soient destinées à devenir des méthodes ou non) dont les résultats dépendent de leurs arguments (pas de choses modifiables comme self! -) et gardent donc un mémo-dict séparé.
Alex Martelli

Réponses:

206

À partir de Python 3.2, il existe un décorateur intégré:

@functools.lru_cache(maxsize=100, typed=False)

Decorator pour envelopper une fonction avec un appelable de mémorisation qui enregistre jusqu'à la taille maximale des appels les plus récents. Cela peut gagner du temps lorsqu'une fonction coûteuse ou liée aux E / S est régulièrement appelée avec les mêmes arguments.

Exemple de cache LRU pour le calcul des nombres de Fibonacci :

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

>>> print([fib(n) for n in range(16)])
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]

>>> print(fib.cache_info())
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)

Si vous êtes bloqué avec Python 2.x, voici une liste d'autres bibliothèques de mémorisation compatibles:

Paolo Moretti
la source
le backport peut maintenant être trouvé ici: pypi.python.org/pypi/backports.functools_lru_cache
Frederick Nord
@gerrit en théorie, cela fonctionne pour les objets hachables en général - bien que certains objets hachables ne soient égaux que s'ils sont le même objet (comme les objets définis par l'utilisateur sans fonction __hash __ () explicite).
Jonathan
1
@Jonathan Cela fonctionne, mais à tort. Si je passe un argument hachable et mutable et que je change la valeur de l'objet après le premier appel de la fonction, le second appel renverra l'objet modifié, pas l'original. Ce n'est certainement pas ce que souhaite l'utilisateur. Pour que cela fonctionne avec des arguments mutables, il faudrait lru_cachefaire une copie du résultat de sa mise en cache, et aucune copie de ce type n'est effectuée dans l' functools.lru_cacheimplémentation. Cela risquerait également de créer des problèmes de mémoire difficiles à trouver lorsqu'il est utilisé pour mettre en cache un objet volumineux.
gerrit
@gerrit Pourriez-vous suivre ici: stackoverflow.com/questions/44583381/... ? Je n'ai pas entièrement suivi votre exemple.
Jonathan
28

Il semble que vous ne demandiez pas un décorateur de mémorisation à usage général (c'est-à-dire que vous n'êtes pas intéressé par le cas général où vous voulez mettre en cache les valeurs de retour pour différentes valeurs d'argument). Autrement dit, vous aimeriez avoir ceci:

x = obj.name  # expensive
y = obj.name  # cheap

tandis qu'un décorateur de mémorisation polyvalent vous donnerait ceci:

x = obj.name()  # expensive
y = obj.name()  # cheap

Je soumets que la syntaxe d'appel de méthode est un meilleur style, car elle suggère la possibilité d'un calcul coûteux tandis que la syntaxe de propriété suggère une recherche rapide.

[Mise à jour: Le décorateur de mémorisation basé sur les classes que j'avais lié et cité ici précédemment ne fonctionne pas pour les méthodes. Je l'ai remplacé par une fonction de décorateur.] Si vous êtes prêt à utiliser un décorateur de mémorisation à usage général, en voici une simple:

def memoize(function):
  memo = {}
  def wrapper(*args):
    if args in memo:
      return memo[args]
    else:
      rv = function(*args)
      memo[args] = rv
      return rv
  return wrapper

Exemple d'utilisation:

@memoize
def fibonacci(n):
  if n < 2: return n
  return fibonacci(n - 1) + fibonacci(n - 2)

Un autre décorateur de mémorisation avec une limite sur la taille du cache peut être trouvé ici .

Cuisine Nathan
la source
Aucun des décorateurs mentionnés dans toutes les réponses ne fonctionne pour les méthodes! Probablement parce qu'ils sont basés sur la classe. Un seul moi est passé? D'autres fonctionnent bien, mais il est cruel de stocker des valeurs dans des fonctions.
Tobias
2
Je pense que vous pouvez rencontrer un problème si les arguments ne sont pas hachables.
Inconnu
1
@Unknown Oui, le premier décorateur que j'ai cité ici est limité aux types hachables. Celui d'ActiveState (avec la limite de taille du cache) décortique les arguments dans une chaîne (hachable) qui est bien sûr plus chère mais plus générale.
Nathan Kitchen
@vanity Merci d'avoir souligné les limites des décorateurs basés sur les classes. J'ai révisé ma réponse pour montrer une fonction de décorateur, qui fonctionne pour les méthodes (j'ai en fait testé celle-ci).
Nathan Kitchen
1
@SiminJie Le décorateur n'est appelé qu'une seule fois, et la fonction encapsulée qu'il renvoie est la même que celle utilisée pour tous les différents appels à fibonacci. Cette fonction utilise toujours le même memodictionnaire.
Nathan Kitchen
22
class memorize(dict):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args):
        return self[args]

    def __missing__(self, key):
        result = self[key] = self.func(*key)
        return result

Exemples d'utilisations:

>>> @memorize
... def foo(a, b):
...     return a * b
>>> foo(2, 4)
8
>>> foo
{(2, 4): 8}
>>> foo('hi', 3)
'hihihi'
>>> foo
{(2, 4): 8, ('hi', 3): 'hihihi'}
acmerfight
la source
Étrange! Comment cela marche-t-il? Cela ne ressemble pas aux autres décorateurs que j'ai vus.
PascalVKooten
1
Cette solution renvoie une TypeError si l'on utilise des arguments de mot-clé, par exemple foo (3, b = 5)
kadee
1
Le problème de la solution, c'est qu'elle n'a pas de limite de mémoire. En ce qui concerne les arguments nommés, vous pouvez simplement les ajouter à __ call__ et __ missing__ comme ** nargs
Leonid Mednikov
16

functools.cached_propertyDécorateur Python 3.8

https://docs.python.org/dev/library/functools.html#functools.cached_property

cached_propertyde Werkzeug a été mentionné à l' adresse : https://stackoverflow.com/a/5295190/895245 mais une version supposée dérivée sera fusionnée dans 3.8, ce qui est génial.

Ce décorateur peut être considéré comme une mise en cache @propertyou comme un nettoyeur @functools.lru_cachelorsque vous n'avez aucun argument.

Les documents disent:

@functools.cached_property(func)

Transformez une méthode d'une classe en une propriété dont la valeur est calculée une fois, puis mise en cache en tant qu'attribut normal pendant la durée de vie de l'instance. Similaire à property (), avec l'ajout de la mise en cache. Utile pour les propriétés calculées coûteuses des instances qui sont par ailleurs effectivement immuables.

Exemple:

class DataSet:
    def __init__(self, sequence_of_numbers):
        self._data = sequence_of_numbers

    @cached_property
    def stdev(self):
        return statistics.stdev(self._data)

    @cached_property
    def variance(self):
        return statistics.variance(self._data)

Nouveau dans la version 3.8.

Remarque Ce décorateur requiert que l' attribut dict sur chaque instance soit un mappage mutable. Cela signifie qu'il ne fonctionnera pas avec certains types, tels que les métaclasses (puisque les attributs dict sur les instances de type sont des proxys en lecture seule pour l'espace de noms de classe), et ceux qui spécifient des emplacements sans inclure dict comme l'un des emplacements définis (en tant que telles classes ne fournissez pas du tout un attribut dict ).

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
la source
10

Werkzeug a un cached_propertydécorateur ( docs , source )

Imran
la source
Oui. Cela vaut la peine de faire la distinction avec le cas général de la mémorisation, car la mémorisation standard ne fonctionne pas si la classe n'est pas hachable.
Jameson Quinn
1
Maintenant en Python 3.8: docs.python.org/dev/library/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
9

J'ai codé cette classe de décorateur simple pour mettre en cache les réponses des fonctions. Je le trouve TRÈS utile pour mes projets:

from datetime import datetime, timedelta 

class cached(object):
    def __init__(self, *args, **kwargs):
        self.cached_function_responses = {}
        self.default_max_age = kwargs.get("default_cache_max_age", timedelta(seconds=0))

    def __call__(self, func):
        def inner(*args, **kwargs):
            max_age = kwargs.get('max_age', self.default_max_age)
            if not max_age or func not in self.cached_function_responses or (datetime.now() - self.cached_function_responses[func]['fetch_time'] > max_age):
                if 'max_age' in kwargs: del kwargs['max_age']
                res = func(*args, **kwargs)
                self.cached_function_responses[func] = {'data': res, 'fetch_time': datetime.now()}
            return self.cached_function_responses[func]['data']
        return inner

L'utilisation est simple:

import time

@cached
def myfunc(a):
    print "in func"
    return (a, datetime.now())

@cached(default_max_age = timedelta(seconds=6))
def cacheable_test(a):
    print "in cacheable test: "
    return (a, datetime.now())


print cacheable_test(1,max_age=timedelta(seconds=5))
print cacheable_test(2,max_age=timedelta(seconds=5))
time.sleep(7)
print cacheable_test(3,max_age=timedelta(seconds=5))
Pablo Besada
la source
1
Votre premier @cachedest sans parenthèse. Sinon, il ne renverra l' cachedobjet qu'à la place de myfuncet lorsqu'il est appelé comme myfunc()alors, il innersera toujours retourné comme valeur de retour
Markus Meskanen
6

AVERTISSEMENT: je suis l'auteur de kids.cache .

Vous devriez vérifier kids.cache, il fournit un @cachedécorateur qui fonctionne sur python 2 et python 3. Pas de dépendances, ~ 100 lignes de code. C'est très simple à utiliser, par exemple, avec votre code à l'esprit, vous pouvez l'utiliser comme ceci:

pip install kids.cache

ensuite

from kids.cache import cache
...
class MyClass(object):
    ...
    @cache            # <-- That's all you need to do
    @property
    def name(self):
        return 1 + 1  # supposedly expensive calculation

Ou vous pouvez mettre le @cachedécorateur après le @property(même résultat).

L'utilisation du cache sur une propriété est appelée évaluation paresseuse , kids.cachepeut faire beaucoup plus (cela fonctionne sur la fonction avec tous les arguments, propriétés, tout type de méthodes, et même les classes ...). Pour les utilisateurs avancés, kids.cacheprend en charge cachetoolsqui fournit des magasins de cache sophistiqués pour python 2 et python 3 (cache LRU, LFU, TTL, RR).

REMARQUE IMPORTANTE : le magasin de cache par défaut de kids.cacheest un dict standard, ce qui n'est pas recommandé pour les programmes de longue durée avec des requêtes toujours différentes car cela conduirait à un magasin de mise en cache toujours croissant. Pour cette utilisation, vous pouvez ajouter d'autres magasins de cache en utilisant par exemple ( @cache(use=cachetools.LRUCache(maxsize=2))pour décorer votre fonction / propriété / classe / méthode ...)

vaab
la source
Ce module semble entraîner un temps d'importation lent sur python 2 ~ 0.9s (voir: pastebin.com/raw/aA1ZBE9Z ). Je soupçonne que cela est dû à cette ligne github.com/0k/kids.cache/blob/master/src/kids/__init__.py#L3 (cf points d'entrée setuptools). Je crée un problème pour cela.
Att Righ
Voici un problème pour le github.com/0k/kids.cache/issues/9 ci-dessus .
Att Righ
Cela entraînerait une fuite de mémoire.
Timothy Zhang
@vaab crée une instance cde MyClass, et l'inspecte avec objgraph.show_backrefs([c], max_depth=10), il existe une chaîne de références de l'objet de classe MyClassà c. C'est-à-dire qu'ils cn'auraient jamais été libérés tant qu'ils ne l'avaient pas MyClassété.
Timothy Zhang
@TimothyZhang vous êtes invités et bienvenus à faire part de vos préoccupations sur github.com/0k/kids.cache/issues/10 . Stackoverflow n'est pas le bon endroit pour avoir une discussion appropriée à ce sujet. Et des clarifications supplémentaires sont nécessaires. Merci pour votre avis.
vaab
5

Ah, juste besoin de trouver le bon nom pour cela: " Lazy property evaluation ".

Je fais beaucoup ça aussi; peut-être que j'utiliserai cette recette dans mon code un jour.

Ken Arnold
la source
4

Il y a fastcache , qui est "l'implémentation C de functools.lru_cache Python 3. Fournit une accélération de 10-30x par rapport à la bibliothèque standard."

Identique à la réponse choisie , juste une importation différente:

from fastcache import lru_cache
@lru_cache(maxsize=128, typed=False)
def f(a, b):
    pass

En outre, il est installé dans Anaconda , contrairement aux functools qui doivent être installés .

Romi Kuntsman
la source
1
functoolsfait partie de la bibliothèque standard, le lien que vous avez posté est vers un git fork aléatoire ou autre chose ...
cz
3

Il y a encore un autre exemple de décorateur de mémorisation sur Python Wiki:

http://wiki.python.org/moin/PythonDecoratorLibrary#Memoize

Cet exemple est un peu intelligent, car il ne mettra pas en cache les résultats si les paramètres sont mutables. (vérifiez ce code, c'est très simple et intéressant!)

Denilson Sá Maia
la source
3

Si vous utilisez Django Framework, il a une telle propriété pour mettre en cache une vue ou une réponse de l'utilisation de l'API @cache_page(time)et il peut également y avoir d'autres options.

Exemple:

@cache_page(60 * 15, cache="special_cache")
def my_view(request):
    ...

Plus de détails peuvent être trouvés ici .

Nikhil Kumar
la source
2

Avec l' exemple Memoize, j'ai trouvé les packages python suivants:

  • cachepy ; Il permet de configurer ttl et / ou le nombre d'appels pour les fonctions mises en cache; En outre, on peut utiliser un cache basé sur des fichiers cryptés ...
  • percache
bulle
la source
1

J'ai implémenté quelque chose comme ça, en utilisant pickle pour la persistance et en utilisant sha1 pour des identifiants courts, presque certainement uniques. Fondamentalement, le cache a haché le code de la fonction et l'historique des arguments pour obtenir un sha1, puis a cherché un fichier avec ce sha1 dans le nom. S'il existait, il l'ouvrit et retourna le résultat; sinon, il appelle la fonction et enregistre le résultat (en option, sauvegarde uniquement si le traitement a pris un certain temps).

Cela dit, je jurerais que j'ai trouvé un module existant qui a fait cela et je me retrouve ici à essayer de trouver ce module ... Le plus proche que je puisse trouver est celui-ci, qui semble à peu près correct: http: //chase-seibert.github. io / blog / 2011/11/23 / pythondjango-disk-based-caching-decorator.html

Le seul problème que je vois avec cela est que cela ne fonctionnerait pas bien pour les grandes entrées car il hache str (arg), ce qui n'est pas unique pour les tableaux géants.

Ce serait bien s'il y avait un protocole unique_hash () qui avait une classe renvoyant un hachage sécurisé de son contenu. Je l'ai essentiellement implémenté manuellement pour les types qui m'intéressaient.

Ben
la source
1

Essayez joblib http://pythonhosted.org/joblib/memory.html

from joblib import Memory
memory = Memory(cachedir=cachedir, verbose=0)
@memory.cache
    def f(x):
        print('Running f(%s)' % x)
        return x
Dror Hilman
la source
1

Si vous utilisez Django et souhaitez mettre en cache des vues, consultez la réponse de Nikhil Kumar .


Mais si vous voulez mettre en cache N'IMPORTE QUEL résultat de fonction, vous pouvez utiliser django-cache-utils .

Il réutilise les caches Django et fournit un cacheddécorateur facile à utiliser :

from cache_utils.decorators import cached

@cached(60)
def foo(x, y=0):
    print 'foo is called'
    return x+y
Greg Dubicki
la source
1

@lru_cache n'est pas parfait avec les valeurs de fonction par défaut

mon memdécorateur:

import inspect


def get_default_args(f):
    signature = inspect.signature(f)
    return {
        k: v.default
        for k, v in signature.parameters.items()
        if v.default is not inspect.Parameter.empty
    }


def full_kwargs(f, kwargs):
    res = dict(get_default_args(f))
    res.update(kwargs)
    return res


def mem(func):
    cache = dict()

    def wrapper(*args, **kwargs):
        kwargs = full_kwargs(func, kwargs)
        key = list(args)
        key.extend(kwargs.values())
        key = hash(tuple(key))
        if key in cache:
            return cache[key]
        else:
            res = func(*args, **kwargs)
            cache[key] = res
            return res
    return wrapper

et code pour les tests:

from time import sleep


@mem
def count(a, *x, z=10):
    sleep(2)
    x = list(x)
    x.append(z)
    x.append(a)
    return sum(x)


def main():
    print(count(1,2,3,4,5))
    print(count(1,2,3,4,5))
    print(count(1,2,3,4,5, z=6))
    print(count(1,2,3,4,5, z=6))
    print(count(1))
    print(count(1, z=10))


if __name__ == '__main__':
    main()

résultat - seulement 3 fois avec le sommeil

mais avec @lru_cachece sera 4 fois, car ceci:

print(count(1))
print(count(1, z=10))

sera calculé deux fois (mauvais fonctionnement avec les valeurs par défaut)

Sublimer
la source