Thursday, September 17, 2009

Python Function Decorators: Caching a Class Methods Output

I've been writing a class and I've noticed that one of its methods may could create a lot of overhead to our file server when it is used often. And its result was not changing so often. So trying to get all the data over and over again is something I should avoid.

I've decided to create some kind of caching system to overcome this problem. I've changed the function so it was caching its own result, and it was not trying to get the data over and over again before some definite time is passes. It was working very nice. But later on I've noticed several other functions those had to have this functionality.

So I've decided to use a decorator that decorates the functions with that ability. Here is the decorator:
#######################################################################
class CachedMethod(object):
    """caches the result of a class method inside the instance
    """
    
    
    
    #----------------------------------------------------------------------
    def __init__(self, method):
        # record the unbound-method and the name
        #print "running __init__ in CachcedMethod"
        
        if not isinstance( method, property ):
            self._method = method
            self._name = method.__name__
            self._isProperty = False
        else:
            self._method = method.fget
            self._name = method.fget.__name__
            self._isProperty = True
        
        self._obj = None
    
    
    
    #----------------------------------------------------------------------
    def __get__(self, inst, cls):
        """use __get__ to get the instance object
        and create the attributes
        
        if it is  a property call __call__
        """
        #print "running __get__ in CachcedMethod"
        
        self._obj = inst
        
        if not hasattr( self._obj, self._name + "._data"):
            #print "creating the data"
            setattr ( self._obj, self._name + "._data", None )
            setattr ( self._obj, self._name + "._lastQueryTime", 0 )
            setattr ( self._obj, self._name + "._maxTimeDelta", 60 )
            #print "finished creating data"
        
        if self._isProperty:
            return self.__call__()
        else:
            return self
    
    
    
    #----------------------------------------------------------------------
    def __call__(self, *args, **kwargs):
        """
        """
        #print "running __call__ in CachedMethod"
        
        delta = time.time() - getattr( self._obj, self._name + "._lastQueryTime" )
        
        if delta > getattr(self._obj, self._name + "._maxTimeDelta") or getattr(self._obj, self._name + "._data" ) == None:
            # call the function and store the result as a cache
            #print "caching the data"
            data = self._method(self._obj, *args, **kwargs )
            setattr( self._obj, self._name + "._data", data )
            
            # zero the time
            lastQueryTime = time.time()
            setattr( self._obj, self._name + "._lastQueryTime", time.time() )
        #else:
            #print "returning the cached data"
        
        return getattr( self._obj, self._name + "._data" )
    
    
    
    #----------------------------------------------------------------------
    def __repr__(self):
        """Return the function's representation
        """
        objectsRepr = str(self._obj)
        objectsName = objectsRepr.split(' ')[0].split('.')[-1]
        cachedObjectsRepr = ''
        return cachedObjectsRepr


You can use that anywhere you want, but beware that the caching is time dependent, not input dependent.

No comments: