簡體   English   中英

使用帶有字典參數的 @functools.lru_cache

[英]Using @functools.lru_cache with dictionary arguments

我有一個方法,將(除其他外)字典作為參數。 該方法解析字符串,字典為某些子字符串提供替換,因此它不必是可變的。

這個函數經常被調用,並且在冗余元素上被調用,所以我認為緩存它會提高它的效率。

但是,正如您可能已經猜到的,由於dict是可變的,因此不可散列, @functools.lru_cache無法修飾我的函數。 那么我該如何克服呢?

如果它只需要標准庫類和方法,那就加分點。 理想情況下,如果它在標准庫中存在某種我沒有見過的frozendict ,那將是我的一天。

PS: namedtuple只作為最后的手段,因為它需要一個大的語法轉變。

不要使用自定義的可哈希字典,而是使用它並避免重新發明輪子! 這是一個凍結的字典,所有的都可以散列。

https://pypi.org/project/frozendict/

代碼:

def freezeargs(func):
    """Transform mutable dictionnary
    Into immutable
    Useful to be compatible with cache
    """

    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        args = tuple([frozendict(arg) if isinstance(arg, dict) else arg for arg in args])
        kwargs = {k: frozendict(v) if isinstance(v, dict) else v for k, v in kwargs.items()}
        return func(*args, **kwargs)
    return wrapped

進而

@freezeargs
@lru_cache
def func(...):
    pass

代碼取自@fast_cen 的回答

注意:這不適用於遞歸數據結構; 例如,您可能有一個列表參數,它是不可散列的。 邀請您進行遞歸包裝,以便深入數據結構並凍結每個dict和每個list元組。

(我知道 OP 不再需要解決方案,但我來這里是為了尋找相同的解決方案,因此將其留給后代)

這是一個使用@mhyfritz 技巧的裝飾器。

def hash_dict(func):
    """Transform mutable dictionnary
    Into immutable
    Useful to be compatible with cache
    """
    class HDict(dict):
        def __hash__(self):
            return hash(frozenset(self.items()))

    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        args = tuple([HDict(arg) if isinstance(arg, dict) else arg for arg in args])
        kwargs = {k: HDict(v) if isinstance(v, dict) else v for k, v in kwargs.items()}
        return func(*args, **kwargs)
    return wrapped

只需在 lru_cache 之前添加它。

@hash_dict
@functools.lru_cache()
def your_function():
    ...

像這樣創建一個可散列的dict類怎么樣:

class HDict(dict):
    def __hash__(self):
        return hash(frozenset(self.items()))

substs = HDict({'foo': 'bar', 'baz': 'quz'})
cache = {substs: True}

如何子類化namedtuple並通過x["key"]添加訪問權限?

class X(namedtuple("Y", "a b c")):
    def __getitem__(self, item):
        if isinstance(item, int):
            return super(X, self).__getitem__(item)
        return getattr(self, item)

這是一個可以像functools.lru_cache一樣使用的裝飾器。 但這是針對僅采用一個參數的函數,該參數是具有可散列值平面映射,並且固定maxsize為 64。對於您的用例,您必須調整此示例或您的客戶端代碼。 另外,要單獨設置maxsize ,必須實現另一個裝飾器,但由於我不需要它,因此我沒有考慮過這個問題。

from functools import (_CacheInfo, _lru_cache_wrapper, lru_cache,
                       partial, update_wrapper)
from typing import Any, Callable, Dict, Hashable

def lru_dict_arg_cache(func: Callable) -> Callable:
    def unpacking_func(func: Callable, arg: frozenset) -> Any:
        return func(dict(arg))

    _unpacking_func = partial(unpacking_func, func)
    _cached_unpacking_func = \
        _lru_cache_wrapper(_unpacking_func, 64, False, _CacheInfo)

    def packing_func(arg: Dict[Hashable, Hashable]) -> Any:
        return _cached_unpacking_func(frozenset(arg.items()))

    update_wrapper(packing_func, func)
    packing_func.cache_info = _cached_unpacking_func.cache_info
    return packing_func


@lru_dict_arg_cache
def uppercase_keys(arg: dict) -> dict:
    """ Yelling keys. """
    return {k.upper(): v for k, v in arg.items()}


assert uppercase_keys.__name__ == 'uppercase_keys'
assert uppercase_keys.__doc__ == ' Yelling keys. '
assert uppercase_keys({'ham': 'spam'}) == {'HAM': 'spam'}
assert uppercase_keys({'ham': 'spam'}) == {'HAM': 'spam'}
cache_info = uppercase_keys.cache_info()
assert cache_info.hits == 1
assert cache_info.misses == 1
assert cache_info.maxsize == 64
assert cache_info.currsize == 1
assert uppercase_keys({'foo': 'bar'}) == {'FOO': 'bar'}
assert uppercase_keys({'foo': 'baz'}) == {'FOO': 'baz'}
cache_info = uppercase_keys.cache_info()
assert cache_info.hits == 1
assert cache_info.misses == 3
assert cache_info.currsize == 3

對於更通用的方法,可以使用第三方庫中的裝飾器@cachetools.cache 並將適當的函數設置為key

在決定暫時為我們的用例刪除 lru 緩存之后,我們仍然想出了一個解決方案。 這個裝飾器使用 json 序列化和反序列化發送到緩存的 args/kwargs。 適用於任意數量的參數。 將其用作函數的裝飾器而不是 @lru_cache。 最大大小設置為 1024。

def hashable_lru(func):
    cache = lru_cache(maxsize=1024)

    def deserialise(value):
        try:
            return json.loads(value)
        except Exception:
            return value

    def func_with_serialized_params(*args, **kwargs):
        _args = tuple([deserialise(arg) for arg in args])
        _kwargs = {k: deserialise(v) for k, v in kwargs.items()}
        return func(*_args, **_kwargs)

    cached_function = cache(func_with_serialized_params)

    @wraps(func)
    def lru_decorator(*args, **kwargs):
        _args = tuple([json.dumps(arg, sort_keys=True) if type(arg) in (list, dict) else arg for arg in args])
        _kwargs = {k: json.dumps(v, sort_keys=True) if type(v) in (list, dict) else v for k, v in kwargs.items()}
        return cached_function(*_args, **_kwargs)
    lru_decorator.cache_info = cached_function.cache_info
    lru_decorator.cache_clear = cached_function.cache_clear
    return lru_decorator

基於@Cedar answer ,按照建議添加遞歸以進行深度凍結:

def deep_freeze(thing):
    from collections.abc import Collection, Mapping, Hashable
    from frozendict import frozendict
    if thing is None or isinstance(thing, str):
        return thing
    elif isinstance(thing, Mapping):
        return frozendict({k: deep_freeze(v) for k, v in thing.items()})
    elif isinstance(thing, Collection):
        return tuple(deep_freeze(i) for i in thing)
    elif not isinstance(thing, Hashable):
        raise TypeError(f"unfreezable type: '{type(thing)}'")
    else:
        return thing


def deep_freeze_args(func):
    import functools

    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        return func(*deep_freeze(args), **deep_freeze(kwargs))
    return wrapped

暫無
暫無

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

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