2013-07-31 46 views
0

我有一個函數執行昂貴的操作,並經常調用;但是,操作只需要執行一次 - 其結果可以被緩存。函數計算一次,緩存結果,並從緩存無限(Python)返回

我試圖使一個無限的發電機,但我沒有得到我預期的效果:

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

爲什麼不是g發電機?

>>> g 
<function g at 0x1093de488> 

編輯:這很好,如果這個方法行不通,但我需要東西執行完全像一個普通的功能,比如:

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

爲什麼不只是緩存結果在變量或函數屬性?或者查看一下您可以通過Google找到的其中一種Python備忘錄配方。 – user2357112

+0

@ user2357112注意。兩個都在答案中提到。 – 2rs2ts

+0

我無法弄清楚你的名字,有沒有什麼我沒有得到?對於ts​​ ?,這是2complicated4me – Stephan

回答

2

這是一個簡單的緩存修飾器。它沒有考慮到參數的任何變化,它只是在第一次調用之後返回相同的結果。有更奇特的緩存每個輸入組合的結果(「記憶」)。

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 

用法:

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

如果你希望寫你的功能出於某種原因(可能的結果是在每次調用略有不同,但仍然是一個費時的初始設置的發電機,否則你只是想要C風格的靜態變量,讓你的函數要記住從一個呼叫狀態的某些位到下),你可以使用這個裝飾:

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 

用法:

@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 

一個Python 2.5或更高版本使用.send()允許參數傳遞給發電機的每次迭代如下(注意:**kwargs不支持):

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

有沒有理由讓'結果'列表? – 2rs2ts

+2

所以它可以通過包裝函數進行修改(另外,它使得測試能夠查看函數是否已經被稱爲簡單函數)。你也可以在Python 3.x中使用'nonlocal'。 – kindall

+0

爲什麼它必須是一個列表才能被修改?爲什麼不能'result = None',後來'result = func(* args,** kwargs)'? – 2rs2ts

6

g()是發電機功能。調用它會返回生成器。然後您需要使用該生成器來獲取您的值。通過循環,例如,或致電它next()

gen = g() 
value = next(gen) 

注意再次調用g()將重新計算相同的值,併產生一個新的發電機

您可能只想使用全局緩存該值。把它作爲一個屬性功能可以工作:

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

一個更好的選擇是使用memoization。您可以創建一個memoize裝飾器,您可以使用它來包裝要緩存結果的任何函數。你可以找到一些很好的實現here

+0

非常棒。爲我的目的精心製作,但如果我需要富有表現力的功能,我最終會在功能屬性hack上使用記憶配方。 – 2rs2ts

+0

重新閱讀此鏈接我注意到[此配方](http://wiki.python.org/moin/PythonDecoratorLibrary#Alternate_memoize_as_nested_functions)與@ kindall的答案非常相似。 – 2rs2ts

3

更好的方法:@functools.lru_cache(maxsize=None)。 python 2.7已經是backported,或者你可以自己寫。

我偶爾會犯這樣的:

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

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

'functools.lru_cache'需要相當大的性能損失。除非你需要一個跟蹤多個值的緩存,需要緩存管理和每個函數參數的值,否則我不會爲此而煩惱。 –

+0

請參閱[爲什麼未編譯,在Python 3中重複使用的正則表達式速度太慢]?(http://stackoverflow.com/q/14756790)有關LRU緩存錯誤應用的示例。 Python開發人員在下一個版本發佈中取消了這一改變。 –

+0

如果我可以使用外部軟件包(僅用於此目的),我會使用'functools.lru_cache'。它看起來非常酷(並且簡單)。你可以解釋@MartijnPieters爲什麼?我不能只使用'maxsize = 1'嗎? – 2rs2ts