Lazy attributes - Utilities

Skip to content
Back to top
View this page
Edit this page

# Lazy attributes

AUTHORS:

- Nicolas Thiery (2008): Initial version

- Nils Bruin (2013-05): Cython version

class sage.misc.lazy_attribute.lazy_attribute(f)

  Bases: `_lazy_attribute`

  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 easily override a given attribute; you don’t need to call the super class constructor, etc.

  Technically, a `lazy_attribute` is a non-data descriptor (see Invoking Descriptors in the Python reference manual).

  EXAMPLES:

  We create a class whose instances have a lazy attribute `x`:

  Sage

      sage: class A():
      ....:     def __init__(self):
      ....:         self.a=2 # just to have some data to calculate from
      ....:
      ....:     @lazy_attribute
      ....:     def x(self):
      ....:         print("calculating x in A")
      ....:         return self.a + 1
      ....:

  Python

      >>> from sage.all import *
      >>> class A():
      ...     def __init__(self):
      ...         self.a=Integer(2) # just to have some data to calculate from
      ....:
      >>>     @lazy_attribute
      ...     def x(self):
      ...         print("calculating x in A")
      ...         return self.a + Integer(1)
      ....:

  For an instance `a` of `A`, `a.x` is calculated the first time it is accessed, and then stored as a usual attribute:

  Sage

      sage: a = A()
      sage: a.x
      calculating x in A
      3
      sage: a.x
      3

  [Python]

      >>> from sage.all import *
      >>> a = A()
      >>> a.x
      calculating x in A
      3
      >>> a.x
      3

  Implementation details

  We redo the same example, but opening the hood to see what happens to the internal dictionary of the object:

  Sage

      sage: a = A()
      sage: a.__dict__
      {'a': 2}
      sage: a.x
      calculating x in A
      3
      sage: a.__dict__
      {'a': 2, 'x': 3}
      sage: a.x
      3
      sage: timeit('a.x') # random
      625 loops, best of 3: 89.6 ns per loop

  Python

      >>> from sage.all import *
      >>> a = A()
      >>> a.__dict__
      {'a': 2}
      >>> a.x
      calculating x in A
      3
      >>> a.__dict__
      {'a': 2, 'x': 3}
      >>> a.x
      3
      >>> 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

      sage: a = A()
      sage: a.x = 4
      sage: a.x
      4
      sage: a.__dict__
      {'a': 2, 'x': 4}

  [Python]

      >>> from sage.all import *
      >>> a = A()
      >>> a.x = Integer(4)
      >>> a.x
      4
      >>> a.__dict__
      {'a': 2, 'x': 4}

  Class binding results in the lazy attribute itself:

  Sage

      sage: A.x
      <sage.misc.lazy_attribute.lazy_attribute object at ...>

  Python

      >>> from sage.all import *
      >>> A.x
      <sage.misc.lazy_attribute.lazy_attribute object at ...>

  Conditional definitions

  The function calculating the attribute may return NotImplemented to declare that, after all, it is not able to do it. In that case, the attribute lookup proceeds in the super class hierarchy:

  Sage

      sage: class B(A):
      ....:     @lazy_attribute
      ....:     def x(self):
      ....:         if hasattr(self, "y"):
      ....:             print("calculating x from y in B")
      ....:             return self.y
      ....:         else:
      ....:             print("y not there; B does not define x")
      ....:             return NotImplemented
      ....:
      sage: b = B()
      sage: b.x
      y not there; B does not define x
      calculating x in A
      3
      sage: b = B()
      sage: b.y = 1
      sage: b.x
      calculating x from y in B
      1

  [Python]

      >>> from sage.all import *
      >>> class B(A):
      ...     @lazy_attribute
      ...     def x(self):
      ...         if hasattr(self, "y"):
      ...             print("calculating x from y in B")
      ...             return self.y
      ...         else:
      ...             print("y not there; B does not define x")
      ...             return NotImplemented
      ....:
      >>> b = B()
      >>> b.x
      y not there; B does not define x
      calculating x in A
      3
      >>> b = B()
      >>> b.y = Integer(1)
      >>> b.x
      calculating x from y in B
      1

  Attribute existence testing

  Testing for the existence of an attribute with hasattr currently always triggers its full calculation, which may not be desirable when the calculation is expensive:

  Sage

      sage: a = A()
      sage: hasattr(a, "x")
      calculating x in A
      True

  Python

      >>> from sage.all import *
      >>> a = A()
      >>> hasattr(a, "x")
      calculating x in A
      True

  It would be great if we could take over the control somehow, if at all possible without a special implementation of hasattr, so as to allow for something like:

  Sage

      sage: class A ():
      ....:     @lazy_attribute
      ....:     def x(self, existence_only=False):
      ....:         if existence_only:
      ....:             print("testing for x existence")
      ....:             return True
      ....:         else:
      ....:             print("calculating x in A")
      ....:             return 3
      ....:
      sage: a = A()
      sage: hasattr(a, "x") # todo: not implemented
      testing for x existence
      sage: a.x
      calculating x in A
      3
      sage: a.x
      3

  [Python]

      >>> from sage.all import *
      >>> class A ():
      ...     @lazy_attribute
      ...     def x(self, existence_only=False):
      ...         if existence_only:
      ...             print("testing for x existence")
      ...             return True
      ...         else:
      ...             print("calculating x in A")
      ...             return Integer(3)
      ....:
      >>> a = A()
      >>> hasattr(a, "x") # todo: not implemented
      testing for x existence
      >>> a.x
      calculating x in A
      3
      >>> a.x
      3

  Here is a full featured example, with both conditional definition and existence testing:

  Sage

      sage: class B(A):
      ....:     @lazy_attribute
      ....:     def x(self, existence_only=False):
      ....:         if hasattr(self, "y"):
      ....:             if existence_only:
      ....:                 print("testing for x existence in B")
      ....:                 return True
      ....:             else:
      ....:                 print("calculating x from y in B")
      ....:                 return self.y
      ....:         else:
      ....:             print("y not there; B does not define x")
      ....:             return NotImplemented
      ....:
      sage: b = B()
      sage: hasattr(b, "x") # todo: not implemented
      y not there; B does not define x
      testing for x existence
      True
      sage: b.x
      y not there; B does not define x
      calculating x in A
      3
      sage: b = B()
      sage: b.y = 1
      sage: hasattr(b, "x") # todo: not implemented
      testing for x existence in B
      True
      sage: b.x
      calculating x from y in B
      1

  Python

      >>> from sage.all import *
      >>> class B(A):
      ...     @lazy_attribute
      ...     def x(self, existence_only=False):
      ...         if hasattr(self, "y"):
      ...             if existence_only:
      ...                 print("testing for x existence in B")
      ...                 return True
      ...             else:
      ...                 print("calculating x from y in B")
      ...                 return self.y
      ...         else:
      ...             print("y not there; B does not define x")
      ...             return NotImplemented
      ....:
      >>> b = B()
      >>> hasattr(b, "x") # todo: not implemented
      y not there; B does not define x
      testing for x existence
      True
      >>> b.x
      y not there; B does not define x
      calculating x in A
      3
      >>> b = B()
      >>> b.y = Integer(1)
      >>> hasattr(b, "x") # todo: not implemented
      testing for x existence in B
      True
      >>> b.x
      calculating x from y in B
      1

  lazy attributes and introspection

  Todo

  Make the following work nicely:

  Sage

      sage: b.x?                # todo: not implemented
      sage: b.x??               # todo: not implemented

  Python

      >>> from sage.all import *
      >>> b.x?                # todo: not implemented
      >>> b.x??               # todo: not implemented

  Right now, the first one includes the doc of this class, and the second one brings up the code of this class, both being not very useful.

  Lazy attributes and Cython

  This attempts to check that lazy attributes work with built-in functions like cpdef methods:

  Sage

      sage: class A:
      ....:     def __len__(x):
      ....:         return int(5)
      ....:     len = lazy_attribute(len)
      ....:
      sage: A().len
      5

  Python

      >>> from sage.all import *
      >>> class A:
      ...     def __len__(x):
      ...         return int(Integer(5))
      ...     len = lazy_attribute(len)
      ....:
      >>> A().len
      5

  Since Issue #11115, extension classes derived from `Parent` can inherit a lazy attribute, such as `element_class`:

  Sage

      sage: cython_code = ["from sage.structure.parent cimport Parent",
      ....: "from sage.structure.element cimport Element",
      ....: "cdef class MyElement(Element): pass",
      ....: "cdef class MyParent(Parent):",
      ....: "    Element = MyElement"]
      sage: cython('\n'.join(cython_code))                                            # needs sage.misc.cython
      sage: P = MyParent(category=Rings())                                            # needs sage.misc.cython
      sage: P.element_class    # indirect doctest                                     # needs sage.misc.cython
      <class '...MyElement'>

  [Python]

      >>> from sage.all import *
      >>> cython_code = ["from sage.structure.parent cimport Parent",
      ... "from sage.structure.element cimport Element",
      ... "cdef class MyElement(Element): pass",
      ... "cdef class MyParent(Parent):",
      ... "    Element = MyElement"]
      >>> cython('\n'.join(cython_code))                                            # needs sage.misc.cython
      >>> P = MyParent(category=Rings())                                            # needs sage.misc.cython
      >>> P.element_class    # indirect doctest                                     # needs sage.misc.cython
      <class '...MyElement'>

  About descriptor specifications

  The specifications of descriptors (see 3.4.2.3 Invoking Descriptors in the Python reference manual) are incomplete w.r.t. inheritance, and maybe even ill-implemented. We illustrate this on a simple class hierarchy, with an instrumented descriptor:

  Sage

      sage: class descriptor():
      ....:     def __get__(self, obj, cls):
      ....:         print(cls)
      ....:         return 1
      sage: class A():
      ....:     x = descriptor()
      sage: class B(A):
      ....:     pass
      ....:

  Python

      >>> from sage.all import *
      >>> class descriptor():
      ...     def __get__(self, obj, cls):
      ...         print(cls)
      ...         return Integer(1)
      >>> class A():
      ...     x = descriptor()
      >>> class B(A):
      ...     pass
      ....:

  This is fine:

  Sage

      sage: A.x
      <class '__main__.A'>
      1

  [Python]

      >>> from sage.all import *
      >>> A.x
      <class '__main__.A'>
      1

  The behaviour for the following case is not specified (see Instance Binding) when `x` is not in the dictionary of `B` but in that of some super category:

  Sage

      sage: B().x
      <class '__main__.B'>
      1

  Python

      >>> from sage.all import *
      >>> B().x
      <class '__main__.B'>
      1

  It would seem more natural (and practical!) to get `A` rather than `B`.

  From the specifications for Super Binding, it would be expected to get `A` and not `B` as cls parameter:

  Sage

      sage: super(B, B()).x
      <class '__main__.B'>
      1

  [Python]

      >>> from sage.all import *
      >>> super(B, B()).x
      <class '__main__.B'>
      1

  Due to this, the natural implementation runs into an infinite loop in the following example:

  Sage

      sage: class A():
      ....:     @lazy_attribute
      ....:     def unimplemented_A(self):
      ....:         return NotImplemented
      ....:     @lazy_attribute
      ....:     def unimplemented_AB(self):
      ....:         return NotImplemented
      ....:     @lazy_attribute
      ....:     def unimplemented_B_implemented_A(self):
      ....:         return 1
      ....:
      sage: class B(A):
      ....:     @lazy_attribute
      ....:     def unimplemented_B(self):
      ....:         return NotImplemented
      ....:     @lazy_attribute
      ....:     def unimplemented_AB(self):
      ....:         return NotImplemented
      ....:     @lazy_attribute
      ....:     def unimplemented_B_implemented_A(self):
      ....:         return NotImplemented
      ....:
      sage: class C(B):
      ....:     pass
      ....:

  Python

      >>> from sage.all import *
      >>> class A():
      ...     @lazy_attribute
      ...     def unimplemented_A(self):
      ...         return NotImplemented
      ...     @lazy_attribute
      ...     def unimplemented_AB(self):
      ...         return NotImplemented
      ...     @lazy_attribute
      ...     def unimplemented_B_implemented_A(self):
      ...         return Integer(1)
      ....:
      >>> class B(A):
      ...     @lazy_attribute
      ...     def unimplemented_B(self):
      ...         return NotImplemented
      ...     @lazy_attribute
      ...     def unimplemented_AB(self):
      ...         return NotImplemented
      ...     @lazy_attribute
      ...     def unimplemented_B_implemented_A(self):
      ...         return NotImplemented
      ....:
      >>> class C(B):
      ...     pass
      ....:

  This is the simplest case where, without workaround, we get an infinite loop:

  Sage

      sage: hasattr(B(), "unimplemented_A") # todo: not implemented
      False

  [Python]

      >>> from sage.all import *
      >>> hasattr(B(), "unimplemented_A") # todo: not implemented
      False

  Todo

  Improve the error message:

  Sage

      sage: B().unimplemented_A # todo: not implemented
      Traceback (most recent call last):
      ...
      AttributeError: 'super' object has no attribute 'unimplemented_A'...

  Python

      >>> from sage.all import *
      >>> B().unimplemented_A # todo: not implemented
      Traceback (most recent call last):
      ...
      AttributeError: 'super' object has no attribute 'unimplemented_A'...

  We now make some systematic checks:

  Sage

      sage: B().unimplemented_A
      Traceback (most recent call last):
      ...
      AttributeError: '...' object has no attribute 'unimplemented_A'...
      sage: B().unimplemented_B
      Traceback (most recent call last):
      ...
      AttributeError: '...' object has no attribute 'unimplemented_B'...
      sage: B().unimplemented_AB
      Traceback (most recent call last):
      ...
      AttributeError: '...' object has no attribute 'unimplemented_AB'...
      sage: B().unimplemented_B_implemented_A
      1

      sage: C().unimplemented_A()
      Traceback (most recent call last):
      ...
      AttributeError: '...' object has no attribute 'unimplemented_A'...
      sage: C().unimplemented_B()
      Traceback (most recent call last):
      ...
      AttributeError: '...' object has no attribute 'unimplemented_B'...
      sage: C().unimplemented_AB()
      Traceback (most recent call last):
      ...
      AttributeError: '...' object has no attribute 'unimplemented_AB'...
      sage: C().unimplemented_B_implemented_A # todo: not implemented
      1

  Python

      >>> from sage.all import *
      >>> B().unimplemented_A
      Traceback (most recent call last):
      ...
      AttributeError: '...' object has no attribute 'unimplemented_A'...
      >>> B().unimplemented_B
      Traceback (most recent call last):
      ...
      AttributeError: '...' object has no attribute 'unimplemented_B'...
      >>> B().unimplemented_AB
      Traceback (most recent call last):
      ...
      AttributeError: '...' object has no attribute 'unimplemented_AB'...
      >>> B().unimplemented_B_implemented_A
      1

      >>> C().unimplemented_A()
      Traceback (most recent call last):
      ...
      AttributeError: '...' object has no attribute 'unimplemented_A'...
      >>> C().unimplemented_B()
      Traceback (most recent call last):
      ...
      AttributeError: '...' object has no attribute 'unimplemented_B'...
      >>> C().unimplemented_AB()
      Traceback (most recent call last):
      ...
      AttributeError: '...' object has no attribute 'unimplemented_AB'...
      >>> C().unimplemented_B_implemented_A # todo: not implemented
      1

class sage.misc.lazy_attribute.lazy_class_attribute(f)

  Bases: `lazy_attribute`

  A lazy class attribute for a class is like a usual class attribute, except that, instead of being computed when the class is constructed, it is computed on the fly the first time it is accessed, either through the class itself or through one of its objects.

  This is very similar to `lazy_attribute` except that the attribute is a class attribute. More precisely, once computed, the lazy class attribute is stored in the class rather than in the object. The lazy class attribute is only computed once for all the objects:

  Sage

      sage: class Cl():
      ....:     @lazy_class_attribute
      ....:     def x(cls):
      ....:          print("computing x")
      ....:          return 1
      sage: Cl.x
      computing x
      1
      sage: Cl.x
      1

  Python

      >>> from sage.all import *
      >>> class Cl():
      ...     @lazy_class_attribute
      ...     def x(cls):
      ...          print("computing x")
      ...          return Integer(1)
      >>> Cl.x
      computing x
      1
      >>> Cl.x
      1

  As for a any usual class attribute it is also possible to access it from an object:

  Sage

      sage: b = Cl()
      sage: b.x
      1

  [Python]

      >>> from sage.all import *
      >>> b = Cl()
      >>> b.x
      1

  First access from an object also properly triggers the computation:

  Sage

      sage: class Cl1():
      ....:     @lazy_class_attribute
      ....:     def x(cls):
      ....:          print("computing x")
      ....:          return 1
      sage: Cl1().x
      computing x
      1
      sage: Cl1().x
      1

  Python

      >>> from sage.all import *
      >>> class Cl1():
      ...     @lazy_class_attribute
      ...     def x(cls):
      ...          print("computing x")
      ...          return Integer(1)
      >>> Cl1().x
      computing x
      1
      >>> Cl1().x
      1

  Warning

  The behavior of lazy class attributes with respect to inheritance is not specified. It currently depends on the evaluation order:

  Sage

      sage: class A():
      ....:     @lazy_class_attribute
      ....:     def x(cls):
      ....:          print("computing x")
      ....:          return str(cls)
      ....:     @lazy_class_attribute
      ....:     def y(cls):
      ....:          print("computing y")
      ....:          return str(cls)
      sage: class B(A):
      ....:     pass

      sage: A.x
      computing x
      "<class '__main__.A'>"
      sage: B.x
      "<class '__main__.A'>"

      sage: B.y
      computing y
      "<class '__main__.B'>"
      sage: A.y
      computing y
      "<class '__main__.A'>"
      sage: B.y
      "<class '__main__.B'>"

  Python

      >>> from sage.all import *
      >>> class A():
      ...     @lazy_class_attribute
      ...     def x(cls):
      ...          print("computing x")
      ...          return str(cls)
      ...     @lazy_class_attribute
      ...     def y(cls):
      ...          print("computing y")
      ...          return str(cls)
      >>> class B(A):
      ...     pass

      >>> A.x
      computing x
      "<class '__main__.A'>"
      >>> B.x
      "<class '__main__.A'>"

      >>> B.y
      computing y
      "<class '__main__.B'>"
      >>> A.y
      computing y
      "<class '__main__.A'>"
      >>> B.y
      "<class '__main__.B'>"