2013-07-31 1 views
0

J'ai une fonction qui effectue une opération coûteuse et s'appelle souvent; mais, l'opération doit seulement être effectuée une fois - son résultat pourrait être mis en cache.Fonction qui calcule une fois, met en cache le résultat et retourne à partir du cache à l'infini (Python)

J'ai essayé de faire un générateur infini, mais je n'ai pas les résultats que j'attendais:

>>> def g(): 
...  result = "foo" 
...  while True: 
...   yield result 
... 
>>> g() 
<generator object g at 0x1093db230> # why didn't it give me "foo"? 

Pourquoi ne pas g un générateur?

>>> g 
<function g at 0x1093de488> 

Modifier: il est très bien si cette approche ne fonctionne pas, mais je besoin quelque chose qui effectue exactement comme une fonction régulière, comme suit:

>>> [g() for x in range(3)] 
["foo", "foo", "foo"] 
+0

Pourquoi ne pas simplement cache le résultat dans une variable ou un attribut de fonction? Ou consultez l'une des recettes de mémo de Python que vous pouvez trouver via Google. – user2357112

+0

@ user2357112 Noté. Les deux ont été mentionnés dans les réponses. – 2rs2ts

+0

Je n'arrive pas à comprendre votre nom, y at-il quelque chose que je ne reçois pas? Trop à ts?, C'est 2complicated4me – Stephan

Répondre

2

Voici un décorateur de cache simple. Il ne tient pas compte des variations de paramètres, il renvoie juste le même résultat après le premier appel. Il y a des colombophiles là-bas qui mettent en cache le résultat pour chaque combinaison d'entrées ("memoization").

import functools 

def callonce(func): 

    result = [] 

    @functools.wraps(func) 
    def wrapper(*args, **kwargs): 
     if not result: 
      result.append(func(*args, **kwargs)) 
     return result[0] 

    return wrapper 

Utilisation:

@callonce 
def long_running_function(x, y, z): 
    # do something expensive with x, y, and z, producing result 
    return result 

Si vous préférez écrire votre fonction en tant que générateur pour une raison quelconque (peut-être le résultat est légèrement différent sur chaque appel, mais il y a encore une configuration initiale du temps , ou bien vous voulez simplement des variables statiques de type C qui permettent à votre fonction de se rappeler un peu peu de l'état d'un appel à l'autre), vous pouvez utiliser ce décorateur:

import functools 

def gen2func(generator): 

    gen = [] 

    @functools.wraps(generator) 
    def wrapper(*args, **kwargs): 
     if not gen: 
      gen.append(generator(*args, **kwargs)) 
     return next(gen[0]) 

    return wrapper 

utilisation:

@gen2func 
def long_running_function_in_generator_form(x, y, z): 
    # do something expensive with x, y, and z, producing result 
    while True: 
     yield result 
     result += 1 # for example 

A Python 2.5 ou version ultérieure qui utilise .send() pour permettre les paramètres à transmettre à chaque itération du générateur est comme suit (notez que **kwargs ne sont pas pris en charge):

import functools 

def gen2func(generator): 

    gen = [] 

    @functools.wraps(generator) 
    def wrapper(*args): 
     if not gen: 
      gen.append(generator(*args)) 
      return next(gen[0]) 
     return gen[0].send(args) 

    return wrapper 

@gen2func 
def function_with_static_vars(a, b, c): 
    # time-consuming initial setup goes here 
    # also initialize any "static" vars here 
    while True: 
     # do something with a, b, c 
     a, b, c = yield  # get next a, b, c 
+1

Y a-t-il une raison pour que vous fassiez une liste? – 2rs2ts

+2

Donc il peut être modifié par la fonction wrapper (aussi, il fait un test pour voir si la fonction a déjà été appelée simple). Vous pouvez également utiliser 'nonlocal' dans Python 3.x. – kindall

+0

Pourquoi doit-il être une liste pour être modifié? Pourquoi ne peut-il pas être 'result = None', et plus tard' result = func (* args, ** kwargs) '? – 2rs2ts

6

g() est un générateur fonction. L'appeler renvoie le générateur. Vous devez ensuite utiliser ce générateur pour obtenir vos valeurs. En boucle, par exemple, ou en appelant next() sur elle:

gen = g() 
value = next(gen) 

Notez que l'appel g() nouveau calcule à nouveau la même valeur et produire un nouveau générateur .

Vous pouvez simplement utiliser un global pour mettre en cache la valeur. Le stockage comme un attribut sur la fonction pourrait fonctionner:

def g(): 
    if not hasattr(g, '_cache'): 
     g._cache = 'foo' 
    return g._cache 
1

Une meilleure option serait d'utiliser memoization. Vous pouvez créer un décorateur memoize que vous pouvez utiliser pour envelopper n'importe quelle fonction pour laquelle vous souhaitez mettre en cache les résultats. Vous pouvez trouver quelques bonnes implémentations here.

+0

Très génial. Élaborer pour mes fins, mais si j'ai besoin de la puissance expressive, je vais finir par utiliser une recette mémoization sur l'attribut fonction hack. – 2rs2ts

+0

Relecture de ce lien J'ai remarqué que [cette recette] (http://wiki.python.org/moin/PythonDecoratorLibrary#Alternate_memoize_as_nested_functions) est très similaire à la réponse de @ kindall. – 2rs2ts

3

Une meilleure façon: @functools.lru_cache(maxsize=None). Python 2.7 est à backported, ou vous pouvez écrire le vôtre.

Je suis parfois coupable de faire:

def foo(): 
    if hasattr(foo, 'cache'): 
     return foo.cache 

    # do work 
    foo.cache = result 
    return result 
+0

Le 'functools.lru_cache' prend une grosse pénalité de performance. Sauf si vous avez besoin d'un cache qui suit plusieurs valeurs, nécessite la gestion du cache et des valeurs par paramètres de fonction, je ne serais pas déranger avec cela. –

+0

Voir [Pourquoi les expressions régulières non recompilées sont-elles utilisées beaucoup plus lentement dans Python 3?] (Http://stackoverflow.com/q/14756790) pour un exemple d'application incorrecte du cache LRU. Les développeurs Python ont annulé ce changement dans la prochaine version. –

+0

J'utiliserais 'functools.lru_cache' si je pouvais utiliser des paquets externes (juste à cet effet). Il semble très cool (et mort simple). @MartijnPieters pouvez-vous expliquer pourquoi? Ne pourrais-je pas simplement utiliser 'maxsize = 1'? – 2rs2ts

0

Vous pouvez également tirer parti Beaker et son cache. Il a également une tonne de extensions.

Questions connexes