簡體   English   中英

是否有一種 Pythonic 方式來支持 Python 中 memoize 裝飾器的關鍵字參數?

[英]Is there a pythonic way to support keyword arguments for a memoize decorator in Python?

所以我最近問了一個關於記憶的問題並得到了一些很好的答案,現在我想把它提升到一個新的水平。 經過相當多的谷歌搜索,我找不到能夠緩存采用關鍵字參數的函數的 memoize 裝飾器的參考實現。 事實上,他們中的大多數只是使用*args作為緩存查找的鍵,這意味着如果你想記住一個接受列表或字典作為參數的函數,它也會中斷。

就我而言,該函數的第一個參數本身就是一個唯一標識符,適合用作緩存查找的 dict 鍵,但是我希望能夠使用關鍵字參數並仍然訪問相同的緩存。 我的意思是, my_func('unique_id', 10)my_func(foo=10, func_id='unique_id')都應該返回相同的緩存結果。

為了做到這一點,我們需要一種干凈的 Pythonic 方式來表達“檢查與第一個參數對應的關鍵字)”。 這就是我想出的:

class memoize(object):
    def __init__(self, cls):
        if type(cls) is FunctionType:
            # Let's just pretend that the function you gave us is a class.
            cls.instances = {}
            cls.__init__ = cls
        self.cls = cls
        self.__dict__.update(cls.__dict__)

    def __call__(self, *args, **kwargs):
        """Return a cached instance of the appropriate class if it exists."""
        # This is some dark magic we're using here, but it's how we discover
        # that the first argument to Photograph.__init__ is 'filename', but the
        # first argument to Camera.__init__ is 'camera_id' in a general way.
        delta = 2 if type(self.cls) is FunctionType else 1
        first_keyword_arg = [k
            for k, v in inspect.getcallargs(
                self.cls.__init__,
                'self',
                'first argument',
                *['subsequent args'] * (len(args) + len(kwargs) - delta)).items()
                    if v == 'first argument'][0]
        key = kwargs.get(first_keyword_arg) or args[0]
        print key
        if key not in self.cls.instances:
            self.cls.instances[key] = self.cls(*args, **kwargs)
        return self.cls.instances[key]

瘋狂的是,這確實有效。 例如,如果你這樣裝飾:

@memoize
class FooBar:
    instances = {}

    def __init__(self, unique_id, irrelevant=None):
        print id(self)

然后從您的代碼中,您可以調用FooBar('12345', 20)FooBar(irrelevant=20, unique_id='12345')實際獲得 FooBar 的相同實例 然后您可以為第一個參數定義一個具有不同名稱的不同類,因為它以通用方式工作(即,裝飾器不需要知道關於它正在裝飾的類的任何特定信息,以便它工作)。

問題是,這真是一團糟;-)

它起作用是因為inspect.getcallargs返回一個字典,將定義的關鍵字映射到您提供的參數,所以我向它提供了一些虛假參數,然后檢查字典中第一個傳遞的參數。

如果這樣的東西真的存在的話,會更好的是類似於inspect.getcallargs ,它返回兩種參數統一為參數列表而不是關鍵字參數的字典。 這將允許這樣的事情:

def __call__(self, *args, **kwargs):
    key = inspect.getcallargsaslist(self.cls.__init__, None, *args, **kwargs)[1]
    if key not in self.cls.instances:
        self.cls.instances[key] = self.cls(*args, **kwargs)
    return self.cls.instances[key]

我可以看到解決此問題的另一種方法是使用inspect.getcallargs提供的 dict 直接作為查找緩存鍵,但這需要一種可重復的方法來從相同的哈希中生成相同的字符串,這是我聽說過的不能依賴(我想我必須在對鍵進行排序后自己構建字符串)。

有沒有人對此有任何想法? 想要使用關鍵字參數調用函數並緩存結果是錯誤的嗎? 或者只是非常困難?

我建議如下:

import inspect

class key_memoized(object):
    def __init__(self, func):
       self.func = func
       self.cache = {}

    def __call__(self, *args, **kwargs):
        key = self.key(args, kwargs)
        if key not in self.cache:
            self.cache[key] = self.func(*args, **kwargs)
        return self.cache[key]

    def normalize_args(self, args, kwargs):
        spec = inspect.getargs(self.func.__code__).args
        return dict(kwargs.items() + zip(spec, args))

    def key(self, args, kwargs):
        a = self.normalize_args(args, kwargs)
        return tuple(sorted(a.items()))

例子:

@key_memoized
def foo(bar, baz, spam):
    print 'calling foo: bar=%r baz=%r spam=%r' % (bar, baz, spam)
    return bar + baz + spam

print foo(1, 2, 3)
print foo(1, 2, spam=3)         #memoized
print foo(spam=3, baz=2, bar=1) #memoized

請注意,您還可以擴展key_memoized並覆蓋其key()方法以提供更具體的記憶策略,例如忽略某些參數:

class memoize_by_bar(key_memoized):
    def key(self, args, kwargs):
        return self.normalize_args(args, kwargs)['bar']

@memoize_by_bar
def foo(bar, baz, spam):
    print 'calling foo: bar=%r baz=%r spam=%r' % (bar, baz, spam)
    return bar

print foo('x', 'ignore1', 'ignore2')
print foo('x', 'ignore3', 'ignore4')

試試lru_cache

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

裝飾器用記憶調用來包裝函數,最多保存最近調用的 maxsize。 當使用相同的參數定期調用昂貴的或 I/O 綁定的函數時,它可以節省時間。

lru_cache 在 python 3.2 中添加,但可以向后移植到 2.x

你可以看看我的包: https : //github.com/Yiling-J/cacheme ,實際上我為所有 args/kwargs 使用了一個容器:

@cacheme(key=lambda c: 'cat:{name}'.format(name=c.cat.name))
def get_cat(self, cat):
    return some_function(cat)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM