Login

Cached model property decorator (like @property)

Author:
homunq
Posted:
November 29, 2011
Language:
Python
Version:
1.3
Score:
1 (after 1 ratings)

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

  1. Template tag - list punctuation for a list of items by shapiromatron 3 months ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 3 months, 1 week ago
  3. Serializer factory with Django Rest Framework by julio 10 months, 1 week ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 10 months, 4 weeks ago
  5. Help text hyperlinks by sa2812 11 months, 3 weeks ago

Comments

Please login first before commenting.