Ticket #5783: lazy_attributes-fixes-5783-final.patch

File lazy_attributes-fixes-5783-final.patch, 22.2 KB (added by nthiery, 12 years ago)
  • doc/en/reference/misc.rst

    # HG changeset patch
    # User Nicolas M. Thiery <nthiery@users.sf.net>
    # Date 1242715727 25200
    # Node ID d6dbb33825e6eb964e34b9d9ac7ef15ec6b22903
    # Parent  268d1efbf60f6c24ad9c49cb9c91ff0437e42340
    #5783: Lazy attribute fixes and improvements
    
     - fix infinite recursion bug
     - adds support for cpdefs methods
     - fix ReST doc (indentation, ::, ...)
     - fix introspection
     - adds a hook for this in sage.misc.sageinspect.sage_getsourcelines
    
    diff --git a/doc/en/reference/misc.rst b/doc/en/reference/misc.rst
    a b Miscellaneous 
    1515   sage/misc/functional
    1616   sage/misc/latex
    1717   sage/misc/latex_macros
     18   sage/misc/lazy_attribute
    1819   sage/misc/log
    1920   sage/misc/persist
    2021   sage/misc/func_persist
  • sage/misc/lazy_attribute.py

    diff --git a/sage/misc/lazy_attribute.py b/sage/misc/lazy_attribute.py
    a b  
     1"""
     2Lazy attributes
     3"""
     4
     5#*****************************************************************************
     6#       Copyright (C) 2008 Nicolas M. Thiery <nthiery at users.sf.net>
     7#
     8#  Distributed under the terms of the GNU General Public License (GPL)
     9#
     10#    This code is distributed in the hope that it will be useful,
     11#    but WITHOUT ANY WARRANTY; without even the implied warranty of
     12#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     13#    General Public License for more details.
     14#
     15#  The full text of the GPL is available at:
     16#
     17#                  http://www.gnu.org/licenses/
     18#*****************************************************************************
     19
    120class lazy_attribute(object):
    221    r"""
    322    A lazy attribute for an object is like a usual attribute, except
    class lazy_attribute(object): 
    1534    Invoking Descriptors in the Python reference manual).
    1635
    1736    EXAMPLES:
    18     We create a class whose instances have a lazy attribute x
    19       sage: class A(object):
    20       ...       def __init__(self):
    21       ...           self.a=2 # just to have some data to calculate from
    22       ...
    23       ...       @lazy_attribute
    24       ...       def x(self):
    25       ...           print "calculating x in A"
    26       ...           return self.a + 1
    27       ...
     37
     38    We create a class whose instances have a lazy attribute x::
     39
     40        sage: class A(object):
     41        ...       def __init__(self):
     42        ...           self.a=2 # just to have some data to calculate from
     43        ...
     44        ...       @lazy_attribute
     45        ...       def x(self):
     46        ...           print "calculating x in A"
     47        ...           return self.a + 1
     48        ...
    2849
    2950    For an instance a of A, a.x is calculated the first time it is accessed,
    30     and then stored as a usual attribute:
    31       sage: a = A()
    32       sage: a.x
    33       calculating x in A
    34       3
    35       sage: a.x
    36       3
     51    and then stored as a usual attribute::
     52   
     53        sage: a = A()
     54        sage: a.x
     55        calculating x in A
     56        3
     57        sage: a.x
     58        3
     59
     60    .. rubric:: Implementation details
    3761
    3862    We redo the same example, but opening the hood to see what happens to
    39     the internal dictionary of the object:
    40       sage: a = A()
    41       sage: a.__dict__
    42       {'a': 2}
    43       sage: a.x
    44       calculating x in A
    45       3
    46       sage: a.__dict__
    47       {'a': 2, 'x': 3}
    48       sage: a.x
    49       3
    50       sage: timeit('a.x') # random
    51       625 loops, best of 3: 89.6 ns per loop
     63    the internal dictionary of the object::
     64
     65        sage: a = A()
     66        sage: a.__dict__
     67        {'a': 2}
     68        sage: a.x
     69        calculating x in A
     70        3
     71        sage: a.__dict__
     72        {'a': 2, 'x': 3}
     73        sage: a.x
     74        3
     75        sage: timeit('a.x') # random
     76        625 loops, best of 3: 89.6 ns per loop
    5277
    5378    This shows that, after the first calculation, the attribute x
    5479    becomes a usual attribute; in particular, there is no time penalty
    5580    to access it.
    5681   
    5782    A lazy attribute may be set as usual, even before its first access,
    58     in which case the lazy calculation is completely ignored:
     83    in which case the lazy calculation is completely ignored::
    5984
    60       sage: a = A()
    61       sage: a.x = 4
    62       sage: a.x
    63       4
    64       sage: a.__dict__
    65       {'a': 2, 'x': 4}
     85        sage: a = A()
     86        sage: a.x = 4
     87        sage: a.x
     88        4
     89        sage: a.__dict__
     90        {'a': 2, 'x': 4}
    6691
     92    Class binding results in the lazy attribute itself::
    6793
    68     Conditional definitions.
     94        sage: A.x
     95        <sage.misc.lazy_attribute.lazy_attribute object at ...>
     96
     97    .. rubric:: Conditional definitions
    6998
    7099    The function calculating the attribute may return NotImplemented
    71100    to declare that, after all, it is not able to do it. In that case,
    72     the attribute lookup proceeds in the super class hierarchy:
     101    the attribute lookup proceeds in the super class hierarchy::
    73102
    74       sage: class B(A):
    75       ...       @lazy_attribute
    76       ...       def x(self):
    77       ...           if hasattr(self, "y"):
    78       ...               print "calculating x from y in B"
    79       ...               return self.y
    80       ...           else:
    81       ...               print "y not there; B does not define x"
    82       ...               return NotImplemented
    83       ...
    84       sage: b = B()
    85       sage: b.x
    86       y not there; B does not define x
    87       calculating x in A
    88       3
    89       sage: b = B()
    90       sage: b.y = 1
    91       sage: b.x
    92       calculating x from y in B
    93       1
     103        sage: class B(A):
     104        ...       @lazy_attribute
     105        ...       def x(self):
     106        ...           if hasattr(self, "y"):
     107        ...               print "calculating x from y in B"
     108        ...               return self.y
     109        ...           else:
     110        ...               print "y not there; B does not define x"
     111        ...               return NotImplemented
     112        ...
     113        sage: b = B()
     114        sage: b.x
     115        y not there; B does not define x
     116        calculating x in A
     117        3
     118        sage: b = B()
     119        sage: b.y = 1
     120        sage: b.x
     121        calculating x from y in B
     122        1
    94123
    95     Attribute existence testing
     124    .. rubric:: Attribute existence testing
    96125
    97126    Testing for the existence of an attribute with hasattr currently
    98127    always triggers its full calculation, which may not be desirable
    99     when the calculation is expensive:
     128    when the calculation is expensive::
    100129
    101       sage: a = A()
    102       sage: hasattr(a, "x")
    103       calculating x in A
    104       True
     130        sage: a = A()
     131        sage: hasattr(a, "x")
     132        calculating x in A
     133        True
    105134
    106135    It would be great if we could take over the control somehow, if at
    107136    all possible without a special implementation of hasattr, so as to
    108     allow for something like:
     137    allow for something like::
    109138
    110       sage: class A (object):
    111       ...       @lazy_attribute
    112       ...       def x(self, existence_only=False):
    113       ...           if existence_only:
    114       ...               print "testing for x existence"
    115       ...               return True
    116       ...           else:
    117       ...               print "calculating x in A"
    118       ...               return 3
    119       ...
    120       sage: a = A()
    121       sage: hasattr(a, "x") # todo: not implemented
    122       testing for x existence
    123       sage: a.x
    124       calculating x in A
    125       3
    126       sage: a.x
    127       3
     139        sage: class A (object):
     140        ...       @lazy_attribute
     141        ...       def x(self, existence_only=False):
     142        ...           if existence_only:
     143        ...               print "testing for x existence"
     144        ...               return True
     145        ...           else:
     146        ...               print "calculating x in A"
     147        ...               return 3
     148        ...
     149        sage: a = A()
     150        sage: hasattr(a, "x") # todo: not implemented
     151        testing for x existence
     152        sage: a.x
     153        calculating x in A
     154        3
     155        sage: a.x
     156        3
    128157
    129158    Here is a full featured example, with both conditional definition
    130     and existence testing:
     159    and existence testing::
    131160
    132       sage: class B(A):
    133       ...       @lazy_attribute
    134       ...       def x(self, existence_only=False):
    135       ...           if hasattr(self, "y"):
    136       ...               if existence_only:
    137       ...                   print "testing for x existence in B"
    138       ...                   return True
    139       ...               else:
    140       ...                   print "calculating x from y in B"
    141       ...                   return self.y
    142       ...           else:
    143       ...               print "y not there; B does not define x"
    144       ...               return NotImplemented
    145       ...
    146       sage: b = B()
    147       sage: hasattr(b, "x") # todo: not implemented
    148       y not there; B does not define x
    149       testing for x existence
    150       True
    151       sage: b.x
    152       y not there; B does not define x
    153       calculating x in A
    154       3
    155       sage: b = B()
    156       sage: b.y = 1
    157       sage: hasattr(b, "x") # todo: not implemented
    158       testing for x existence in B
    159       True
    160       sage: b.x
    161       calculating x from y in B
    162       1
     161        sage: class B(A):
     162        ...       @lazy_attribute
     163        ...       def x(self, existence_only=False):
     164        ...           if hasattr(self, "y"):
     165        ...               if existence_only:
     166        ...                   print "testing for x existence in B"
     167        ...                   return True
     168        ...               else:
     169        ...                   print "calculating x from y in B"
     170        ...                   return self.y
     171        ...           else:
     172        ...               print "y not there; B does not define x"
     173        ...               return NotImplemented
     174        ...
     175        sage: b = B()
     176        sage: hasattr(b, "x") # todo: not implemented
     177        y not there; B does not define x
     178        testing for x existence
     179        True
     180        sage: b.x
     181        y not there; B does not define x
     182        calculating x in A
     183        3
     184        sage: b = B()
     185        sage: b.y = 1
     186        sage: hasattr(b, "x") # todo: not implemented
     187        testing for x existence in B
     188        True
     189        sage: b.x
     190        calculating x from y in B
     191        1
    163192
    164     TESTS:
     193    .. rubric:: lazy attributes and introspection
     194
     195    TODO: make the following work nicely::
     196
     197        sage: b.x?                # todo: not implemented
     198        sage: b.x??               # todo: not implemented
     199
     200    Right now, the first one includes the doc of this class, and the
     201    second one brings up the code of this class, both being not very
     202    useful.
     203
     204    TESTS::
     205
     206    .. rubric:: Partial support for old style classes
    165207
    166208    Old style and new style classes play a bit differently with
    167     @property and attribute setting:
     209    @property and attribute setting::
    168210
    169       sage: class A:
    170       ...       @property
    171       ...       def x(self):
    172       ...           print "calculating x"
    173       ...           return 3
    174       ...
    175       sage: a = A()
    176       sage: a.x = 4
    177       sage: a.__dict__
    178       {'x': 4}
    179       sage: a.x
    180       4
    181       sage: a.__dict__['x']=5
    182       sage: a.x
    183       5
    184 
    185       sage: class A (object):
    186       ...       @property
    187       ...       def x(self):
    188       ...           print "calculating x"
    189       ...           return 3
    190       ...
    191       sage: a = A()
    192       sage: a.x = 4
    193       Traceback (most recent call last):
    194       ...
    195       AttributeError: can't set attribute
    196       sage: a.__dict__
    197       {}
    198       sage: a.x
    199       calculating x
    200       3
    201       sage: a.__dict__['x']=5
    202       sage: a.x
    203       calculating x
    204       3
     211        sage: class A:
     212        ...       @property
     213        ...       def x(self):
     214        ...           print "calculating x"
     215        ...           return 3
     216        ...
     217        sage: a = A()
     218        sage: a.x = 4
     219        sage: a.__dict__
     220        {'x': 4}
     221        sage: a.x
     222        4
     223        sage: a.__dict__['x']=5
     224        sage: a.x
     225        5
     226       
     227        sage: class A (object):
     228        ...       @property
     229        ...       def x(self):
     230        ...           print "calculating x"
     231        ...           return 3
     232        ...
     233        sage: a = A()
     234        sage: a.x = 4
     235        Traceback (most recent call last):
     236        ...
     237        AttributeError: can't set attribute
     238        sage: a.__dict__
     239        {}
     240        sage: a.x
     241        calculating x
     242        3
     243        sage: a.__dict__['x']=5
     244        sage: a.x
     245        calculating x
     246        3
    205247
    206248    In particular, lazy_attributes need to be implemented as non-data
    207249    descriptors for new style classes, so as to leave access to
    208250    setattr. We now check that this implementation also works for old
    209     style classes (conditional definition does not work yet).
     251    style classes (conditional definition does not work yet)::
    210252
    211       sage: class A:
    212       ...       def __init__(self):
    213       ...           self.a=2 # just to have some data to calculate from
    214       ...
    215       ...       @lazy_attribute
    216       ...       def x(self):
    217       ...           print "calculating x"
    218       ...           return self.a + 1
    219       ...
    220       sage: a = A()
    221       sage: a.__dict__
    222       {'a': 2}
    223       sage: a.x
    224       calculating x
    225       3
    226       sage: a.__dict__
    227       {'a': 2, 'x': 3}
    228       sage: a.x
    229       3
    230       sage: timeit('a.x') # random
    231       625 loops, best of 3: 115 ns per loop
     253        sage: class A:
     254        ...       def __init__(self):
     255        ...           self.a=2 # just to have some data to calculate from
     256        ...
     257        ...       @lazy_attribute
     258        ...       def x(self):
     259        ...           print "calculating x"
     260        ...           return self.a + 1
     261        ...
     262        sage: a = A()
     263        sage: a.__dict__
     264        {'a': 2}
     265        sage: a.x
     266        calculating x
     267        3
     268        sage: a.__dict__
     269        {'a': 2, 'x': 3}
     270        sage: a.x
     271        3
     272        sage: timeit('a.x') # random
     273        625 loops, best of 3: 115 ns per loop
     274       
     275        sage: a = A()
     276        sage: a.x = 4
     277        sage: a.x
     278        4
     279        sage: a.__dict__
     280        {'a': 2, 'x': 4}
     281       
     282        sage: class B(A):
     283        ...       @lazy_attribute
     284        ...       def x(self):
     285        ...           if hasattr(self, "y"):
     286        ...               print "calculating x from y in B"
     287        ...               return self.y
     288        ...           else:
     289        ...               print "y not there; B does not define x"
     290        ...               return NotImplemented
     291        ...
     292        sage: b = B()
     293        sage: b.x                         # todo: not implemented
     294        y not there; B does not define x
     295        calculating x in A
     296        3
     297        sage: b = B()
     298        sage: b.y = 1
     299        sage: b.x
     300        calculating x from y in B
     301        1
    232302
    233       sage: a = A()
    234       sage: a.x = 4
    235       sage: a.x
    236       4
    237       sage: a.__dict__
    238       {'a': 2, 'x': 4}
     303    .. rubric:: lazy_attributes and cpdef functions
    239304
    240       sage: class B(A):
    241       ...       @lazy_attribute
    242       ...       def x(self):
    243       ...           if hasattr(self, "y"):
    244       ...               print "calculating x from y in B"
    245       ...               return self.y
    246       ...           else:
    247       ...               print "y not there; B does not define x"
    248       ...               return NotImplemented
    249       ...
    250       sage: b = B()
    251       sage: b.x                         # todo: not implemented
    252       y not there; B does not define x
    253       calculating x in A
    254       3
    255       sage: b = B()
    256       sage: b.y = 1
    257       sage: b.x
    258       calculating x from y in B
    259       1
     305    This attempts to check that lazy_attributes work with builtin
     306    functions like cpdef methods::
     307
     308        sage: class A:
     309        ...       def __len__(x):
     310        ...           return int(5)
     311        ...       len = lazy_attribute(len)
     312        ...
     313        sage: A().len
     314        5
     315
     316    .. rubric:: About descriptor specifications
     317
     318    The specifications of descriptors (see 3.4.2.3 Invoking
     319    Descriptors in the Python reference manual) are incomplete
     320    w.r.t. inheritence, and maybe even ill-implemented. We illustrate
     321    this on a simple class hierarchy, with an instrumented descriptor::
     322
     323        sage: class descriptor(object):
     324        ...       def __get__(self, obj, cls):
     325        ...           print cls
     326        ...           return 1
     327        ...   
     328        sage: class A(object):
     329        ...       x = descriptor()
     330        ...   
     331        sage: class B(A):
     332        ...       pass
     333        ...
     334
     335    This is fine::
     336
     337        sage: A.x
     338        <class '__main__.A'>
     339        1
     340
     341    The behaviour for the following case is not specified (see Instance Binding)
     342    when x is not in the dictionary of B but in that of some super category::
     343
     344        sage: B().x
     345        <class '__main__.B'>
     346        1
     347
     348    It would seem more natural (and practical!) to get A rather than B.
     349
     350    From the specifications for Super Binding, it would be expected to
     351    get A and not B as cls parameter::
     352   
     353        sage: super(B, B()).x
     354        <class '__main__.B'>
     355        1
     356
     357    Due to this, the natural implementation runs into an infinite loop
     358    in the following example::
     359
     360        sage: class A(object):
     361        ...       @lazy_attribute
     362        ...       def unimplemented_A(self):
     363        ...           return NotImplemented
     364        ...       @lazy_attribute
     365        ...       def unimplemented_AB(self):
     366        ...           return NotImplemented
     367        ...       @lazy_attribute
     368        ...       def unimplemented_B_implemented_A(self):
     369        ...           return 1
     370        ...
     371        sage: class B(A):
     372        ...       @lazy_attribute
     373        ...       def unimplemented_B(self):
     374        ...           return NotImplemented
     375        ...       @lazy_attribute
     376        ...       def unimplemented_AB(self):
     377        ...           return NotImplemented
     378        ...       @lazy_attribute
     379        ...       def unimplemented_B_implemented_A(self):
     380        ...           return NotImplemented
     381        ...
     382        sage: class C(B):
     383        ...       pass
     384        ...
     385
     386    This is the simplest case where, without workaround, we get an infinite loop::
     387
     388        sage: hasattr(B(), "unimplemented_A") # todo: not implemented
     389        False
     390
     391    TODO: improve the error message::
     392
     393        sage: B().unimplemented_A # todo: not implemented
     394        Traceback (most recent call last):
     395        ...
     396        AttributeError: 'super' object has no attribute 'unimplemented_A'
     397
     398    We now make some systematic checks::
     399
     400        sage: B().unimplemented_A
     401        Traceback (most recent call last):
     402        ...
     403        AttributeError: '...' object has no attribute 'unimplemented_A'
     404        sage: B().unimplemented_B
     405        Traceback (most recent call last):
     406        ...
     407        AttributeError: '...' object has no attribute 'unimplemented_B'
     408        sage: B().unimplemented_AB
     409        Traceback (most recent call last):
     410        ...
     411        AttributeError: '...' object has no attribute 'unimplemented_AB'
     412        sage: B().unimplemented_B_implemented_A
     413        1
     414
     415        sage: C().unimplemented_A()
     416        Traceback (most recent call last):
     417        ...
     418        AttributeError: '...' object has no attribute 'unimplemented_A'
     419        sage: C().unimplemented_B()
     420        Traceback (most recent call last):
     421        ...
     422        AttributeError: '...' object has no attribute 'unimplemented_B'
     423        sage: C().unimplemented_AB()
     424        Traceback (most recent call last):
     425        ...
     426        AttributeError: '...' object has no attribute 'unimplemented_AB'
     427        sage: C().unimplemented_B_implemented_A # todo: not implemented
     428        1
     429
    260430    """
    261431
    262432    def __init__(self, f):
     433        """
     434        Constructor for lazy attributes
     435
     436        EXAMPLES::
     437
     438            sage: def f(x):
     439            ...       "doc of f"
     440            ...       return 1
     441            ...
     442            sage: x = lazy_attribute(f); x
     443            <sage.misc.lazy_attribute.lazy_attribute object at ...>
     444            sage: x.__doc__
     445            'doc of f'
     446            sage: x.__name__
     447            'f'
     448            sage: x.__module__
     449            '__main__'
     450        """
    263451        self.f = f
     452        if hasattr(f, "func_doc"):
     453            self.__doc__ = f.func_doc
     454        if hasattr(f, "func_name"):
     455            self.__name__ = f.func_name
     456        if hasattr(f, "__module__"):
     457            self.__module__ = f.__module__
     458
     459    def _sage_src_lines_(self):
     460        """
     461        Returns the source code location for the wrapped function.
     462
     463        EXAMPLES:
     464            sage: from sage.misc.sageinspect import sage_getsourcelines
     465            sage: g = lazy_attribute(banner)
     466            sage: (src, lines) = sage_getsourcelines(g)
     467            sage: src[0]
     468            'def banner():\n'
     469            sage: lines
     470            72
     471
     472        """
     473        from sage.misc.sageinspect import sage_getsourcelines
     474        return sage_getsourcelines(self.f)
     475
    264476       
    265     def __get__(self, a, A):
     477    def __get__(self, a, cls):
     478        """
     479        Implements the attribute access protocol.
     480
     481        EXAMPLES::
     482
     483            sage: class A: pass
     484            sage: def f(x): return 1
     485            ...
     486            sage: f = lazy_attribute(f)
     487            sage: f.__get__(A(), A)
     488            1
     489        """
     490        if a is None: # when doing cls.x for cls a class and x a lazy attribute
     491            return self
    266492        result = self.f(a)
    267493        if result is NotImplemented:
    268             return getattr(super(A, a),self.f.func_name)
    269         setattr(a, self.f.func_name, result)
     494            # Workaround: we make sure that cls is the class
     495            # where the lazy attribute self is actually defined.
     496            # This avoids running into an infinite loop
     497            # See About descriptor specifications
     498            for supercls in cls.__mro__:
     499                if self.f.__name__ in supercls.__dict__ and self is supercls.__dict__[self.f.__name__]:
     500                    cls = supercls
     501            return getattr(super(cls, a),self.f.__name__)
     502        setattr(a, self.f.__name__, result)
    270503        return result
  • sage/misc/sageinspect.py

    diff --git a/sage/misc/sageinspect.py b/sage/misc/sageinspect.py
    a b def sage_getsourcelines(obj, is_binary=F 
    521521    - William Stein
    522522    - Extensions by Nick Alexander
    523523    """
     524
     525    try:
     526        return obj._sage_src_lines_()
     527    except (AttributeError, TypeError):
     528        pass
     529
    524530    # Check if we deal with instance
    525531    if isclassinstance(obj):
    526532        obj=obj.__class__