Cached methods/attributes#
It is common to want to cache class methods and attributes. cached
provides a very simple way to do that. Note that advanced features like LRU caching, etc, are not supported yet.
The main idea is to replace boiler plate like this:
class Example_boiler:
def __init__(self, x):
self._x = x
@property
def xlist(self):
if not hasattr(self, "_xlist"):
print("setting xlist")
self._xlist = [self._x, self._x**2]
return self._xlist
e = Example_boiler(x=2)
print("value of xlist:", e.xlist)
print("value of xlist:", e.xlist)
setting xlist
value of xlist: [2, 4]
value of xlist: [2, 4]
While this works perfectly well, it has two drawbacks:
lots of boiler plate
Ugly if you want to clear out the cached value.
Cached property#
First, lets look at a caching a property. For this we’ll use the cached.prop()
decorator:
from module_utilities import cached
class Example_cached:
def __init__(self, x):
self._x = x
@cached.prop
def xlist(self):
print("setting xlist")
return [self._x, self._x**2]
e = Example_cached(x=2)
print("value of xlist:", e.xlist)
print("value of xlist:", e.xlist)
setting xlist
value of xlist: [2, 4]
value of xlist: [2, 4]
In short, the value is cached to a dictionary self._cache
. This dictionary is created if it doesn’t alread exist.
Note that if using __slots__
, you’ll need to include _cache
. Looking at our example, we see that:
e._cache
{'xlist': [2, 4]}
Cached method#
We can also cache methods using cached.meth()
:
class Example_with_method(Example_cached):
@cached.meth
def yzlist(self, y, z=4):
print("getting ylist")
return self.xlist + [y, z]
e = Example_with_method(x=2)
print("value of yzlist(3, 4) :", e.yzlist(3, 4))
# respects default values
print("value of yzlist(3) :", e.yzlist(3))
# respects named arguments
print("value of yzlist(z=4, y=3):", e.yzlist(z=4, y=3))
print("new value", e.yzlist("y", "z"))
getting ylist
setting xlist
value of yzlist(3, 4) : [2, 4, 3, 4]
value of yzlist(3) : [2, 4, 3, 4]
value of yzlist(z=4, y=3): [2, 4, 3, 4]
getting ylist
new value [2, 4, 'y', 'z']
This results in the cache:
e._cache
{'xlist': [2, 4],
('yzlist', (3, 4), frozenset()): [2, 4, 3, 4],
('yzlist', ('y', 'z'), frozenset()): [2, 4, 'y', 'z']}
cached.meth()
also works with arbitrary *args
and **kwargs
:
class Example_with_methd2(Example_cached):
@cached.meth
def example(self, y=1, z=2, *args, **kwargs):
print("getting example")
return {"y": y, "args": args, "**kwargs": kwargs}
e = Example_with_methd2(x=2)
print(e.example(1, 2, 3, a="a"))
print(e.example(1, 2, 3, a="a"))
getting example
{'y': 1, 'args': (3,), '**kwargs': {'a': 'a'}}
{'y': 1, 'args': (3,), '**kwargs': {'a': 'a'}}
e._cache
{('example', (1, 2, 3), frozenset({('a', 'a')})): {'y': 1,
'args': (3,),
'**kwargs': {'a': 'a'}}}
Note that isn’t perfect though. If you mix what is an arg and what is a kwargs, it will give a different cache:
class Example:
@cached.meth
def example(self, *args, **kwargs):
print("getting example")
return {"args": args, "kwargs": kwargs}
e = Example()
print(e.example(1, 2, x="x", y="y"))
print(e.example(1, 2, "x", y="y"))
getting example
{'args': (1, 2), 'kwargs': {'x': 'x', 'y': 'y'}}
getting example
{'args': (1, 2, 'x'), 'kwargs': {'y': 'y'}}
So use with caution
Clearing cache:#
First, note that the key in _cache
is defaults to the name of the function. You can override this by setting key={value}
when calling the decorator:
class Example:
@cached.prop(key="myprop")
def aprop(self):
print("setting aprop")
return "hello"
x = Example()
print(x.aprop)
print(x._cache)
setting aprop
hello
{'myprop': 'hello'}
Now, what if you want to clear out the cache? For example, if some class variable is changed? For this, use cached.clear()
class Example_clear:
def __init__(self, a):
self._a = a
@property
def a(self):
return self._a
@a.setter
@cached.clear
def a(self, val):
print("clear all from a")
self._a = val
@cached.prop
def aprop(self):
print("setting aprop")
return self.a**2
@cached.prop(key="myprop")
def bprop(self):
print("setting bprop")
return self.a**3
@cached.meth
def meth(self, x):
print("setting meth")
return self.a + x
@cached.clear("myprop")
def meth_that_clears_myprop(self):
pass
@cached.clear("meth")
def meth_that_clears_meth(self):
pass
def print_vals(e):
print("aprop ", e.aprop)
print("bprop ", e.bprop)
print("meth(1)", e.meth(x=1))
print("meth(2)", e.meth(x=2))
e = Example_clear(a=2)
print("\nfirst call:")
print_vals(e)
print("\nsecond call:")
print_vals(e)
first call:
setting aprop
aprop 4
setting bprop
bprop 8
setting meth
meth(1) 3
setting meth
meth(2) 4
second call:
aprop 4
bprop 8
meth(1) 3
meth(2) 4
# reset a value
e.a = 2
print(e._cache)
print("call again:")
print_vals(e)
clear all from a
{}
call again:
setting aprop
aprop 4
setting bprop
bprop 8
setting meth
meth(1) 3
setting meth
meth(2) 4
# clear a single method:
print(e._cache)
e.meth_that_clears_myprop()
print(e._cache)
print_vals(e)
{'aprop': 4, 'myprop': 8, ('meth', (1,), frozenset()): 3, ('meth', (2,), frozenset()): 4}
{'aprop': 4, ('meth', (1,), frozenset()): 3, ('meth', (2,), frozenset()): 4}
aprop 4
setting bprop
bprop 8
meth(1) 3
meth(2) 4
# clearing a method clears all calls to method key
print(e._cache)
e.meth_that_clears_meth()
print(e._cache)
print_vals(e)
{'aprop': 4, ('meth', (1,), frozenset()): 3, ('meth', (2,), frozenset()): 4, 'myprop': 8}
{'aprop': 4, 'myprop': 8}
aprop 4
bprop 8
setting meth
meth(1) 3
setting meth
meth(2) 4