4

I want to use cache using for a specific functions in an API. Instead of modifying the internal code line by line, I want to achieve the same by using technique similar to mock patch. E.g.

@cache_patch('lib.Someobjclass.func1',ttl=200)
@cache_patch('lib.Someotherobjclass.func2',ttl=1000)
function abc(*args, **kwargs):
    '''do stuff'''
    cache1 = someobj.func1(args,kwargs)
    '''do more stuff'''
    cache2 = someotherobj.func2(args,kwargs)

Is there any library or technique that can be used?

Sayok88
  • 2,038
  • 1
  • 14
  • 22
  • 1
    You'll probably want a combination of decorators: https://stackoverflow.com/questions/739654/how-to-make-a-chain-of-function-decorators/1594484#1594484 and importing by string: https://stackoverflow.com/a/8719100/769971 – wholevinski Mar 27 '19 at 13:10

1 Answers1

0

Assumption

It is not 100% clear to me what exact behavior you want, so I will assume that the caching shall only be used during execution of the function to which the decorator is applied.


Necessary bits and pieces

You will need to

  • use functools.lru_cache to realize the caching
  • create a decorator that is able to take additional arguments
  • use importlib to import the class given as first argument to the decorator from string
  • monkey patch the desired method with a cached version

Putting it together

import importlib
from functools import lru_cache


class Addition:
    def __init__(self, a):
        self.a = a

    def uncached_addition(self, b):
        # print is only used to demonstrate if the method is actually called or not
        print(f"Computing {self.a} + {b}")
        return self.a + b


class cache_patch:
    def __init__(self, method_as_str, ttl):
        # split the given path into module, class and method name
        class_as_str, method_name = method_as_str.rsplit(".", 1)
        module_path, class_name = class_as_str.rsplit(".", 1)

        self.clazz = getattr(importlib.import_module(module_path), class_name)
        self.method_name = method_name
        self.ttl = ttl

    def __call__(self, func):
        def wrapped(*args, **kwargs):
            # monkey patch the original method with a cached version
            uncached_method = getattr(self.clazz, self.method_name)
            cached_method = lru_cache(maxsize=self.ttl)(uncached_method)
            setattr(self.clazz, self.method_name, cached_method)

            result = func(*args, **kwargs)

            # replace cached method with original
            setattr(self.clazz, self.method_name, uncached_method)
            return result
        return wrapped


@cache_patch('__main__.Addition.uncached_addition', ttl=128)
def perform_patched_uncached_addition(a, b):
    d = Addition(a=1)
    print("Patched nr. 1\t", d.uncached_addition(2))
    print("Patched nr. 2\t", d.uncached_addition(2))
    print("Patched nr. 3\t", d.uncached_addition(2))
    print()


if __name__ == '__main__':
    perform_patched_uncached_addition(1, 2)
    d = Addition(a=1)
    print("Unpatched nr. 1\t", d.uncached_addition(2))
    print("Unpatched nr. 2\t", d.uncached_addition(2))
    print("Unpatched nr. 3\t", d.uncached_addition(2))

Result

As you can see from the output, calling perform_patched_uncached_addition will only output Computing 1 + 2 once, afterwards the cached result is used:

Computing 1 + 2
Patched call nr. 1:  3
Patched call nr. 2:  3
Patched call nr. 3:  3

Any calls made to the class outside of this function will use the unpatched, non cached version of the method:

Computing 1 + 2
Unpatched call nr. 1:    3
Computing 1 + 2
Unpatched call nr. 2:    3
Computing 1 + 2
Unpatched call nr. 3:    3

Caveats

You certainly need to pay attention if you plan to use this approach in a multithreaded environment. You won't be able to tell at what times the cache patch will be applied.
Also, the caching is done based on the arguments passed to the method, which includes self. This means that every instance of the class will have their own "cache".


Side note

Unlike in the code above functools.lru_cache is used as a decorator in most cases:


class Addition:
    def __init__(self, a):
        self.a = a

    @lru_cache(maxsize=128)
    def cached_addition(self, b):
        print(f"Computing {self.a} + b")
        return self.a + b
dudenr33
  • 1,119
  • 1
  • 10
  • 26