15

I am trying to use lru_cache in Python3 to speed up common queries into our Salesforce database. Below is the relevant code that is supposed to

  • a) convert non-hashable arguments to hashable ones, and
  • b) enable the LRU cache for those objects.

When I try this code, the cache works for calling the functions with no arguments, but it doesn't seem to cache the function calls with arguments. Also, I am not sure of how to order the decorators for the decorated functions.

Note, I am using a class here with class and static methods so I can override the get and get_all methods for different subclasses of Resource.

Please explain what I am doing wrong or could be doing better.

from functools import lru_cache
from functools import wraps

class Resource(object):

    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()))

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

    @staticmethod
    @hash_dict
    @lru_cache
    def get(cls, resource_id, lang='en', fields=None):
        pass

    @classmethod
    @hash_dict
    @lru_cache
    def get_all(cls, lang='en', filters=None, fields=None):
        pass
Akash Mahapatra
  • 2,988
  • 1
  • 14
  • 28
Nicholas Tulach
  • 1,023
  • 3
  • 12
  • 35
  • `dict`s are unhashable on purpose, adding `__hash__` method implementation doesn't save you from possible mutation of `kwargs` inside of a wrapped method – Azat Ibrakov Oct 15 '18 at 05:39

3 Answers3

16

No need for an additional package. The following work as expected:

import functools


class A:

  @staticmethod
  @functools.lru_cache(maxsize=None)
  def build(value):
    print('Creating', value)
    return A()


assert A.build('a') is A.build('a')
assert A.build('b') is A.build('b')
assert A.build('b') is not A.build('a')

Starting python 3.9, you can replace @functools.lru_cache(maxsize=None) by @functools.cache.

Conchylicultor
  • 4,631
  • 2
  • 37
  • 40
3

This should be the answer you are looking for: https://ring-cache.readthedocs.io/en/latest/quickstart.html#method-classmethod-staticmethod

lru_cache supports only simple functions. Ring gives very similar interface but including any kind of descriptor supports.

class Page(object):
    (...)

    @ring.lru()
    @classmethod
    def class_content(cls):
        return cls.base_content

    @ring.lru()
    @staticmethod
    def example_dot_com():
        return requests.get('http://example.com').content

See the link for more details. Note that the example is not LRU.

youknowone
  • 919
  • 6
  • 14
2

IDK if I came to late for you, for here is my response. You got a couple of things wrong.

First, you are overwriting args and kwargs in wapped function definition. This is effectively deleting your function parameters.

Second, you only making inmutable lists in the kwargs case, not in args.

Also, lru_cache is a decorator constructor, so it must be called @lru_cache(). I don't know how it is working to you without it.

Besides, you are declaring def get(cls, ... function as a @staticmethod, then it wouldn't receive a cls argument.

But most importantly, defining a decorator inside a class isn't straightforward, as stated in this medium article. I get what you are trying to do: enforce caching by inheritance, and while it may seem a good idea, it will not work properly. If you override a function, you will have to redecorate it with cache again and this misses the point of declaring decorators inside a class.

To wrap it up, I would save myself problems and declare the decorator in a different class and using it elsewhere. But be careful, as caching class methods isn't straightforward either.


Extra:

I was some weeks ago in a similar situation, wanting to cache a function that accepts a numpy array. I came up with this implementation based on this so response. I just transform the array into a tuple, and reconstruct it again (as I need it to be a mutable array at the end).

def np_cache(*args, **kwargs):
    """LRU cache implementation for functions whose FIRST parameter is a numpy array
    >>> array = np.array([[1, 2, 3], [4, 5, 6]])
    >>> @np_cache(maxsize=256)
    ... def multiply(array, factor):
    ...     print("Calculating...")
    ...     return factor*array
    >>> multiply(array, 2)
    Calculating...
    array([[ 2,  4,  6],
           [ 8, 10, 12]])
    >>> multiply(array, 2)
    array([[ 2,  4,  6],
           [ 8, 10, 12]])
    >>> multiply.cache_info()
    CacheInfo(hits=1, misses=1, maxsize=256, currsize=1)

    """
    def decorator(function):
        @wraps(function)
        def wrapper(np_array, *args, **kwargs):
            hashable_array = array_to_tuple(np_array)
            return cached_wrapper(hashable_array, *args, **kwargs)

        @lru_cache(*args, **kwargs)
        def cached_wrapper(hashable_array, *args, **kwargs):
            array = np.array(hashable_array)
            return function(array, *args, **kwargs)

        def array_to_tuple(np_array):
            """Iterates recursivelly."""
            try:
                return tuple(array_to_tuple(_) for _ in np_array)
            except TypeError:
                return np_array

        # copy lru_cache attributes over too
        wrapper.cache_info = cached_wrapper.cache_info
        wrapper.cache_clear = cached_wrapper.cache_clear

        return wrapper

    return decorator

while this does not directly solve your problem, it could be easily generalized for an arbitrary number of input arguments.

Susensio
  • 820
  • 10
  • 19