2010-07-08 4 views
3

J'ai une méthode avec deux paramètres qui effectue des calculs complexes. Il est appelé très souvent avec les mêmes paramètres, donc j'utilise un dictionnaire pour la mise en cache. À l'heure actuelle cela ressemble quelque chose comme ceci:Mise en cache des résultats d'une fonction avec deux paramètres en Python

def foo(self, a, b): 
    params = frozenset([a, b]) 
    if not params in self._cache: 
     self._cache[params] = self._calculate(a, b) 
    return self._cache[params] 

La raison de la construction du frozenset est que les paramètres peuvent être dans un ordre quelconque, mais le résultat sera le même. Je me demande s'il existe une solution plus simple (et surtout plus efficace) pour cela.

Répondre

0

Votre code a deux problèmes:

(1) Il utilise la méthode ancienne dict.has_key() qui est lent et a disparu en Python 3.x. Au lieu de cela, utilisez "key in dict" ou "key not in dict".

Alors:

def foo(self, a, b): 
    params = frozenset([a, b]) 
    if params in self._cache: 
     self._cache[params] = self._calculate(a, b) 
    return self._cache[params] 

(2) "clé dict" est plus lisible, et expose le problème bien pire: votre code ne fonctionne pas! Si les arguments sont dans le dict, il recalcule. S'ils ne sont pas dans la dict, ils exploseront avec une KeyError. Pensez à copier/coller au lieu de taper à partir de la mémoire.

Alors:

def foo(self, a, b): 
    params = frozenset([a, b]) 
    if params not in self._cache: 
     self._cache[params] = self._calculate(a, b) 
    return self._cache[params] 

(3) Quelques autres suggestions d'efficacité:

def foo(self, a, b): 
    if a < b: 
     params = (a, b) 
    else: 
     params = (b, a) 
    try: 
     return self._cache[params] 
    except KeyError: 
     v = self._cache[params] = self._calculate(a, b) 
     return v 
+0

Le bloc try except ne sera plus efficace que s'il s'attend à atteindre le cache plus de 99% du temps. http://stackoverflow.com/questions/3111195/python-performance-try-except-or-not-in – Wilduck

+0

@Wilduck: Ce test n'a pas pris en compte le temps nécessaire à la méthode _calculate dans un environnement de mise en cache, qui est susceptible de rendre la question «pas dans/essayer/obtenir» plutôt marginale. Les repères de l'application réelle sont indiqués. –

+0

Vous avez raison. Tous mes benchmarks ont indiqué que tout type de réglage suggéré dans l'une des réponses ici n'a aucun impact mesurable sur les performances (bien que la mise en cache de base que j'ai implémentée donne une augmentation de performance x3 sur un petit ensemble de données). –

0

Vous pouvez soit combiner les deux valeurs en une valeur de hachage comme str (a) + '\ 0' + str (b) et ensuite le mettre dans un cache, ou vous pouvez créer un tableau à deux dimensions pour que le cache [a] [b] renvoie la valeur que vous voulez.

Vous pouvez également transformer cette fonctionnalité en une fonction de type @decorator, puis vous pouvez réutiliser le décorateur sur plusieurs fonctions et conserver un emplacement de code pour la mise en cache.

+0

J'aime l'idée avec le décorateur. Mais celui avec le hachage combiné ne semble pas bon, car il en résultera des hachages différents pour 'a, b' et' b, a', ou je manque quelque chose ici. –

+0

@cowboy: vous pouvez remplir à la fois le cache [a] [b] et le cache [b] [a] s'il est symétrique – eruciform

0

Je voudrais juste le stocker dans le cache deux fois, une fois pour chaque commande.

def foo(self, a, b): 
    try: 
     return self._cache[(a, b)] 
    except KeyError: 
     value = self._calculate(a, b) 
     self._cache[(a, b)] = self._cache[(b, a)] = value 
     return value 
0

Vous pouvez utiliser beaker.cache pour cela (http://beaker.groovie.org/index.html)

# define a cache manager 
cache = CacheManager(dict_of_config_options) 

# in your class, apply a cache decorator 
@cache.cache('mycache') 
def foo(self, a,b): 
    return self._calculate 

Je pense que cela fonctionne comme vous le souhaitez par défaut. Je ne sais pas si cela fait partie de la clé. Il suppose que a, b sont pickleable.

+1

Merci, je suis tout pour utiliser une bibliothèque au lieu de réinventer la roue, mais dans ce cas je crois être exagéré. –

2

Il n'y a rien de particulièrement inefficace ou compliqué sur la façon dont vous avez implémenté votre mise en cache; c'est essentiellement ce qui doit arriver. Cependant, ce n'est pas vraiment général.

Vous pouvez implémenter une sorte de stratégie de mise en cache plus généralisée, en utilisant des décorateurs si vous le souhaitez, par commodité. Une approche possible pourrait être:

class Memoizer(object): 
    def __init__(self): 
     self._cache = dict() 

    def memoize_unordered(self, f): 
     def wrapper(s, *args, **kwargs): 
      key = (s, f, frozenset(args), frozenset(kwargs.iteritems())) 
      if key not in self._cache: 
       print 'calculating', args, kwargs 
       self._cache[key] = f(s, *args, **kwargs) 
      return self._cache[key] 
     return wrapper 

    def memoize_ordered(self, f): 
     def wrapper(s, *args, **kwargs): 
      key = (s, f, tuple(args), frozenset(kwargs.iteritems())) 
      if key not in self._cache: 
       print 'calculating', args, kwargs 
       self._cache[key] = f(s, *args, **kwargs) 
      return self._cache[key] 
     return wrapper 

memoizer = Memoizer() 

class Foo(object): 

    @memoizer.memoize_unordered 
    def foo(self, a, b): 
     return self._calculate(a, b) 

    def _calculate(self, a, b): 
     return frozenset([a,b]) 

foo = Foo() 


results = [foo.foo(*a) for a in [(1, 5), (1, 5), (5, 1), (9, 12), (12, 9)]] 
for result in results: 
    print 'RESULT', result 

impression:

calculating (1, 5) {} 
calculating (9, 12) {} 
RESULT frozenset([1, 5]) 
RESULT frozenset([1, 5]) 
RESULT frozenset([1, 5]) 
RESULT frozenset([9, 12]) 
RESULT frozenset([9, 12]) 

L'inconvénient bien sûr, de mettre en œuvre la mise en cache hors de votre objet, est que les données du cache ne soit pas effacé lorsque votre objet disparaît , sauf si vous prenez quelques précautions pour que cela se produise.

+0

+1 pour le décorateur. :) – iamgopal

+0

weakref.WeakKeyDictionary et weakref.WeakValueDictionary sont là pour répondre à la question "L'objet vit éternellement à cause de la référence dans le mémo cache". – PaulMcG

Questions connexes