I am implementing a content-aware caching system for a Django REST API. I would like to develop a component which can be added to existing views that would modify the behavior of the base class by checking the cache and falling back to the base class behavior on a miss.
basically, I have something like this:
class Base:
def get(self, request, *args, **kwargs):
....
return Response
class AnotherBase:
def get(self, request, *args, **kwargs):
....
return Response
class Derived(Base):
pass
class OtherDerived(AnotherBase):
pass
and my initial thought was to do something along the lines of
class Cacheable:
def get(self, request, *args, **kwargs):
cache_key = self.get_cache_key(request)
base_get = #.... and this is the problem
return cache.get(cache_key, base_get(request, *args, **kwargs))
def get_cache_key(self, request):
# .... do stuff
class Derived(Cacheable, Base):
pass
class AnotherDerived(Cacheable, AnotherBase):
pass
So clearly this doesn't work, as I don't know how, or if it's possible, or if it's advisable to access the sibling superclass(es) from a mixin.
My goal is an implementation that allows me to add caching behavior to existing views without touching the internals of the existing classes.
Given a view class, C
, s.t. C.get(request, *args, **kwargs) -> Response
, is there a function, F
, s.t. F(C).get(...
does the cache check before falling back to C.get
? And in this quasi-formal notation, we'll say that adding a mixin to the leftmost parent class in the class definition counts as a function.
Is it more appropriate to use method decorators? or how would a class decorator work?
And then I've seen references to __metaclass__
in researching this, but I'm not clear on what that approach looks like.
This is Python 3.6
The answer was a decorator and some Django
-specific libraries.
from django.utils.decorators import method_decorator
from django.core.cache import cache
def cached_get(cache_key_func=None):
"""
Decorator to be applied via django.utils.decorators.method_decorator
Implements content-aware cache fetching by decorating the "get" method
on a django View
:param cache_key_func: a function of fn(request, *args, **kwargs) --> String
which determines the cache key for the request
"""
def decorator(func):
def cached_func(request, *args, **kwargs):
assert cache_key_func is not None, "cache_key_function is required"
key = cache_key_func(request, *args, **kwargs)
result = cache.get(key)
if result is None:
return func(request, *args, **kwargs)
return Response(result)
return cached_func
return decorator
@method_decorator(cached_get(cache_key_func=get_cache_key), name="get")
class SomeView(BaseView):
...
def get_cache_key(request):
# do arbitrary processing on request, the following is the naïve melody
key = urllib.urlencode(request.query_params)
return key
So the solution is to use Django's built-in method_decorator
which applies its first argument, a decorator, to the decorated class's method, named by the second argument, name
, to method_decorator
. I define a higher-order function, cached_get
, which takes another function as its argument, and returns a curried function (closure, so called). By calling this, with the function get_cache_key
(and not, mind you, invoking that function) I have a decorator that will be applied to the 'get' method on SomeView
.
The decorator itself is a straightforward Python decorator -- in this application, it is cached_func
and the original, undecorated get
method is func
. Thus, cached_func
replaces SomeView.get
, so when SomeView.get
is called, it first checks the cache, but falls back to the undecorated method on a miss.
I'm hopeful this approach provides a balance of generic applicability with content-aware key derivation.