Source code for arsenal.cache.lazy
# TODO:
# - consider allowing lazy things to depend on one another
from types import GeneratorType
[docs]class lazy(object):
"""
Lazily load a property defined by a method. The method wrapped is called
at most once to retrieve the result and the result is reused. If the method
is a generator, the value is stored as a list.
Note: instances must have a `__dict__` attribute in order for this property
to work, i.e. no '__slots__' class attribute.
Implementation detail: this is not implemented as a data descriptor so that
we can completely avoid the function call overhead. If one choses to invoke
`__get__` by hand the property will still work as expected because the
lookup logic is replicated in `__get__` for manual invocation.
"""
def __init__(self, func):
self.__name__ = func.__name__
self.__module__ = func.__module__
self.__doc__ = func.__doc__
self.func = func
def __get__(self, obj, type_=None):
if obj is None:
return self
try:
value = obj.__dict__[self.__name__]
except KeyError:
value = self.func(obj)
if isinstance(value, GeneratorType): # store generators as lists
value = list(value)
obj.__dict__[self.__name__] = value
return value
def __set__(self, obj, value):
raise NotImplementedError
def __delete__(self, obj):
raise NotImplementedError
if __name__ == '__main__':
from collections import defaultdict
class Foo(object):
def __init__(self, x):
self.x = x
self.log = defaultdict(int) # track the number of calls to our cached properties/methods
@lazy
def my_ondemand(self):
self.log['ondemand'] += 1
return 'ON DEMAND'.split() # return something mutable
@lazy
def my_cached(self):
self.log['cachedproperty'] += 1
return 'CACHED PROPERTY'.split()
@lazy
def my_lazy(self):
self.log['my_lazy'] += 1
return 'LAZY PROPERTY'.split()
@lazy
def my_lazy_list(self):
self.log['my_lazy_list'] += 1
for i in range(10):
yield i
def test():
import pickle
def checks(x):
o = x.my_ondemand
c = x.my_cached
l = x.my_lazy
assert x.my_ondemand is o
assert x.my_cached is c
assert x.my_lazy is l
print('x.log:', x.log, sep=' ')
assert min(x.log.values()) == max(x.log.values()) == 1
ll = x.my_lazy_list
assert ll == list(range(10)), ll
assert ll is x.my_lazy_list
print(x.log)
assert all(v == 1 for v in x.log.values())
foo = Foo('XXX')
checks(foo)
foo.log.clear()
print('pickling and unpickling..')
foo2 = pickle.loads(pickle.dumps(foo))
# should still call both lazy properties again.
assert not foo2.log
test()