diff -r 4b523bebfdf9 sage/misc/lazy_attribute.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/sage/misc/lazy_attribute.py Sun Oct 26 14:28:01 2008 +0100
@@ -0,0 +1,201 @@
+def lazy_attribute(f):
+ r"""
+ A lazy attribute for an object is like a usual attribute, except
+ that, instead of being computed when the object is constructed
+ (i.e. in __init__), it is computed on the fly the first time it
+ is accessed.
+
+ For constant values attached to an object, lazy attributes provide
+ a shorter syntax and automatic caching (unlike methods), while
+ playing well with inheritance (like methods): a subclass can
+ easilly override a given attribute; you don't need to call the
+ super class constructor, etc.
+
+ EXAMPLES:
+ We create a class whose instances have a lazy attribute x
+ sage: from sage.misc.lazy_attribute import lazy_attribute
+ sage: class A:
+ ... @lazy_attribute
+ ... def x(self):
+ ... print "calculating x"
+ ... return 3
+ ...
+
+ For an instance a of A, a.x is calculated the first time it is accessed,
+ and then stored as a usual attribute:
+ sage: a = A()
+ sage: a.x
+ calculating x
+ 3
+ sage: a.x
+ 3
+
+ We redo the same example, but opening the hood to see what happens to
+ the internal dictionnary of the object:
+ sage: a = A()
+ sage: a.__dict__
+ {}
+ sage: a.x
+ calculating x
+ 3
+ sage: a.__dict__
+ {'x': 3}
+ sage: a.x
+ 3
+ sage: timeit('a.x') # random
+ 625 loops, best of 3: 89.6 ns per loop
+
+ This shows that, after the first calculation, the attribute x
+ becomes a usual attribute; in particular, there is no time penalty
+ to access it.
+
+ A lazy attribute may be set as usual, even before its first access,
+ in which case the lazy calculation is completely ignored:
+
+ sage: a = A()
+ sage: a.x = 4
+ sage: a.x
+ 4
+ sage: a.__dict__
+ {'x': 4}
+
+ Note: testing for the existence of an attribute with hasattr
+ currently triggers its calculation, which may not be desirable
+ when the calculation is expensive:
+
+ sage: a = A()
+ sage: hasattr(a, "x")
+ calculating x
+ True
+
+ It would be great if we could take over the control:
+ sage: class A:
+ ... @lazy_attribute
+ ... def x(self, hasattr=False):
+ ... if hasattr:
+ ... print "testing for x existence"
+ ... return True
+ ... else:
+ ... print "calculating x"
+ ... return 3
+ ...
+ sage: a = A()
+ sage: hasattr(a, "x") # todo: not implemented
+ testing for x existence
+ sage: a.x
+ calculating x
+ 3
+ sage: a.x
+ 3
+
+ TODO: provide a mean for the x function to *not* define the
+ attribute (the most practical would be by returning None, but
+ there might be cases where we actually would want to set the
+ attribute to None), in which case the lookup should be continued
+ in the upper classes:
+
+ sage: class A:
+ ... @property
+ ... def x(self):
+ ... print "calculating x in A"
+ ... return 3
+ ...
+ sage: class B:
+ ... @property
+ ... def x(self):
+ ... if hasattr(self, "y"):
+ ... print "calculating x from y in B"
+ ... return self.y
+ ...
+ sage: b = B()
+ sage: b.x # todo: not implemented
+ calculating x in A
+ 3
+ sage: b = B()
+ sage: b.y = 1
+ sage: b.x
+ calculating x from y in B
+ 1
+
+
+ For some reason (different lookup rules?), property attributes
+ do not work the same way for instances of object:
+ sage: class A:
+ ... @property
+ ... def x(self):
+ ... print "calculating x"
+ ... return 3
+ ...
+ sage: a = A()
+ sage: a.x = 4
+ sage: a.__dict__
+ {'x': 4}
+ sage: a.x
+ 4
+ sage: a.__dict__['x']=5
+ sage: a.x
+ 5
+
+ sage: class A (object):
+ ... @property
+ ... def x(self):
+ ... print "calculating x"
+ ... return 3
+ ...
+ sage: a = A()
+ sage: a.x = 4
+ Traceback (most recent call last):
+ ...
+ AttributeError: can't set attribute
+ sage: a.__dict__
+ {}
+ sage: a.x
+ calculating x
+ 3
+ sage: a.__dict__['x']=5
+ sage: a.x
+ calculating x
+ 3
+
+ There is currently a workaround implementation for instances of
+ object; however it requires to call get for each subsequent
+ attribute access, which is 10 x slower
+
+ sage: from sage.misc.lazy_attribute import lazy_attribute
+ sage: class A (object):
+ ... @lazy_attribute
+ ... def x(self):
+ ... print "calculating x"
+ ... return 3
+ ...
+ sage: a = A()
+ sage: a.__dict__
+ {}
+ sage: a.x
+ calculating x
+ 3
+ sage: a.__dict__
+ {'x': 3}
+ sage: a.x
+ 3
+ sage: timeit('a.x') # random
+ 625 loops, best of 3: 1.05 µs per loop
+
+ sage: a = A()
+ sage: a.x = 4 # todo: not implemented
+ sage: a.x # todo: not implemented
+ 4
+ sage: a.__dict__ # todo: not implemented
+ {'x': 4}
+ """
+
+ def get(self):
+ if isinstance(self, object):
+ attr = f.func_name
+ if not attr in self.__dict__:
+ self.__dict__[attr] = f(self)
+ return self.__dict__[attr]
+ else:
+ setattr(self, f.func_name, f(self))
+ return getattr(self, f.func_name)
+ return property(get)