Recipe20.4.Caching Attribute Values


Recipe 20.4. Caching Attribute Values

Credit: Denis S. Otkidach

Problem

You want to be able to compute attribute values, either per instance or per class, on demand, with automatic caching.

Solution

Custom descriptors are the right tools for this task:

class CachedAttribute(object):     ''' Computes attribute value and caches it in the instance. '''     def _ _init_ _(self, method, name=None):         # record the unbound-method and the name         self.method = method         self.name = name or method._ _name_ _     def _ _get_ _(self, inst, cls):         if inst is None:             # instance attribute accessed on class, return self             return self         # compute, cache and return the instance's attribute value         result = self.method(inst)         setattr(inst, self.name, result)         return result class CachedClassAttribute(CachedAttribute):     ''' Computes attribute value and caches it in the class. '''     def _ _get_ _(self, inst, cls):         # just delegate to CachedAttribute, with 'cls' as ``instance''         return super(CachedClassAttribute, self)._ _get_ _(cls, cls)

Discussion

If your class instances have attributes that must be computed on demand but don't generally change after they're first computed, custom descriptor CachedAttribute as presented in this recipe is just the ticket. Here is a toy example of use (with Python 2.4 syntax):

class MyObject(object):     def _ _init_ _(self, n):         self.n = n     @CachedAttribute     def square(self):         return self.n * self.n m = MyObject(23) print vars(m)                               # 'square' not there yet # emits: {'n': 23} print m.square                              # ...so it gets computed # emits: 529 print vars(m)                               # 'square' IS there now # emits: {'square': 529, 'n': 23} del m.square                                # flushing the cache print vars(m)                               # 'square' removed   # emits: {'n': 23} m.n = 42 print vars(m) # emits: {'n': 42}                   # still no 'square' print m.square                              # ...so gets recomputed # emits: 1764 print vars(m)                               # 'square' IS there again # emits: {'square': 1764, 'n': 23}

As you see, after the first access to m.square, the square attribute is cached in instance m, so it will not get recomputed for that instance. If you need to flush the cache, for example, to change m.n, so that m.square will get recomputed if it is ever accessed again, just del m.square. Remember, attributes can be removed in Python! To use this code in Python 2.3, remove the decorator syntax @CachedAttribute and insert instead an assignment square = CachedAttribute(square) after the end of the def statement for method square.

Custom descriptor CachedClassAttribute is just a simple variant of CachedAttribute, easily obtained by inheritance: it computes the value by calling a method on the class rather than the instance, and it caches the result on the class, too. This may help when all instances of the class need to see the same cached value. CachedClassAttribute is mostly meant for cases in which you do not need to flush the cache because its _ _get_ _ method usually wipes away the instance descriptor itself:

class MyClass(object):     class_attr = 23     @CachedClassAttribute     def square(cls):         return cls.class_attr * cls.class_attr x = MyClass( ) y = MyClass( ) print x.square # emits: 529 print y.square # emits: 529 del MyClass.square print x.square         # raises an AttributeError exception

However, when you do need a cached class attribute with the ability to occasionally flush it, you can still get it with a little trick. To implement this snippet so it works as intended, just add the statement:

class MyClass(MyClass): pass

right after the end of the class MyClass statement and before generating any instance of MyClass. Now, two class objects are named MyClass, a hidden "base" one that always holds the custom descriptor instance, and an outer "subclass" one that is used for everything else, including making instances and holding the cached value if any. Whether this trick is a reasonable one or whether it's too cute and clever for its own good, is a judgment call you can make for yourself! Perhaps it would be clearer to name the base class MyClassBase and use class MyClass(MyClassBase), rather than use the same name for both classes; the mechanism would work in exactly the same fashion, since it is not dependent on the names of classes.

See Also

Custom descriptors are best documented at Raymond Hettinger's web page: http://users.rcn.com/python/download/Descriptor.htm.



Python Cookbook
Python Cookbook
ISBN: 0596007973
EAN: 2147483647
Year: 2004
Pages: 420

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net