This is a nice decorator for using the cache to save the results of expensive-to-calculate but static-per-instance model properties. There is also a decorator for when the property value is another model, and the contents of the other model should not be cached across requests.
3 levels of caching implemented: outermost: django cache middle: obj._cache (for multiple properties on a single object) * innermost: replace the attribute on this object, so we can entirely avoid running this function a second time.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | from django.core.cache import cache
from django.db import models
#Inspiration snippet:
#
#(I was thinking of using this at the end, but the issues of nesting
#these are not worth the headache)
#
#class cached_property(object):
# '''
# A read-only @property that is only evaluated once per python object. The value
# is cached on the object itself rather than the function or class; this should prevent
# memory leakage.
#
# Model instances are re-instantiated for each request, so this should be the last line
# of defense in a django context.
# '''
# def __init__(self, fget, doc=None):
# self.fget = fget
# self.__doc__ = doc or fget.__doc__
# self.__name__ = fget.__name__
# self.__module__ = fget.__module__
#
# def __get__(self, obj, cls):
# if obj is None:
# return self
# obj.__dict__[self.__name__] = result = self.fget(obj)
# return result
class cached_model_property(object):
"""
To use inside the class declaration for SomeClass
1. First make a subclass of of this:
class cached_someclass_property(cached_model_property):
model_name = "SomeClass"
2. Use as if this were a @property decorator inside SomeClass
This should of course only be used if the property is essentially
static. If the property should return a given model instance, where
the instance id is static but the contents could change, see gives_a
decorator below.
"""
model_name = "<generic>"
def __init__(self, fget, doc=None):
self.fget = fget
self.__doc__ = doc or fget.__doc__
self.__name__ = fget.__name__
self.__module__ = fget.__module__
@staticmethod
def pickle(cuke):
"""Pickling/unpickling is used before saving to the django (outermost) cache."""
return cuke
@staticmethod
def unpickle(dill):
return dill
def __get__(self, obj, cls):
"""Use the descriptor protocol to act as a property.
3 levels of caching implemented:
outermost: django cache
middle: obj._cache (for multiple properties on a single object)
innermost: replace the attribute on this object, so we can entirely avoid
running this function a second time.
"""
#First, handle the "unbound" case
if obj is None:
return self
key = ':'.join((self.model_name, str(obj.id)))
try:
#middle cache pull
obj_cache = obj._cache
except AttributeError:
#outermost cache pull
obj_cache = cache.get(key)
if obj_cache is None:
obj_cache = {}
#cache.set(key, {}) #technically, this might improve multithreaded performance, but not worth it.
#middle cache push
#since this is a mutable reference, further changes below will apply
obj._cache = obj_cache
#unpack property from dict of all cached properties for this object
try:
result = self.unpickle(obj_cache[self.__name__])
except:
#missing - calculate value and add to cache
result = self.fget(obj)
obj_cache[self.__name__] = self.pickle(result)
#outermost cache push
cache.set(key, obj_cache)
#innermost cache push (no pull because this function itself is replaced)
obj.__dict__[self.__name__] = result = self.fget(obj)
#return
return result
MODELS = {}
class cached_typed_model_property(cached_model_property):
@staticmethod
def pickle(cuke):
if isinstance(cuke, models.Model):
cls = cuke.__class__
name = cls.__name__
MODELS[name] = cls
return (name, cuke.id)
try:
if issubclass(cuke, models.Model):
name = cuke.__name__
MODELS[name] = cuke
return (name,)
except TypeError: #issubclass is annoying that way
pass
return cuke #for None
@staticmethod
def unpickle(dill):
if isinstance(dill, tuple):
if len(dill) == 2:
return MODELS[dill[0]].objects.get(id=dill[1])
#exceptions here just mean we run the function again, which will end up adding the model to MODELS
if len(dill) == 1:
return MODELS[dill[0]]
return dill
class cached_uprop(cached_typed_model_property):
model_name = "Users"
|
More like this
- Template tag - list punctuation for a list of items by shapiromatron 11 months, 1 week ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 2 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
- Help text hyperlinks by sa2812 1 year, 7 months ago
Comments
Please login first before commenting.