Cached model property decorator (like @property)

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.

fromdjango.core.cacheimportcachefromdjango.dbimportmodels#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 resultclasscached_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=fgetself.__doc__=docorfget.__doc__self.__name__=fget.__name__self.__module__=fget.__module__@staticmethoddefpickle(cuke):"""Pickling/unpickling is used before saving to the django (outermost) cache."""returncuke@staticmethoddefunpickle(dill):returndilldef__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" caseifobjisNone:returnselfkey=':'.join((self.model_name,str(obj.id)))try:#middle cache pullobj_cache=obj._cacheexceptAttributeError:#outermost cache pullobj_cache=cache.get(key)ifobj_cacheisNone: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 applyobj._cache=obj_cache#unpack property from dict of all cached properties for this objecttry:result=self.unpickle(obj_cache[self.__name__])except:#missing - calculate value and add to cacheresult=self.fget(obj)obj_cache[self.__name__]=self.pickle(result)#outermost cache pushcache.set(key,obj_cache)#innermost cache push (no pull because this function itself is replaced)obj.__dict__[self.__name__]=result=self.fget(obj)#returnreturnresultMODELS={}classcached_typed_model_property(cached_model_property):@staticmethoddefpickle(cuke):ifisinstance(cuke,models.Model):cls=cuke.__class__name=cls.__name__MODELS[name]=clsreturn(name,cuke.id)try:ifissubclass(cuke,models.Model):name=cuke.__name__MODELS[name]=cukereturn(name,)exceptTypeError:#issubclass is annoying that waypassreturncuke#for None@staticmethoddefunpickle(dill):ifisinstance(dill,tuple):iflen(dill)==2:returnMODELS[dill[0]].objects.get(id=dill[1])#exceptions here just mean we run the function again, which will end up adding the model to MODELSiflen(dill)==1:returnMODELS[dill[0]]returndillclasscached_uprop(cached_typed_model_property):model_name="Users"