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:

  1. lots of boiler plate

  2. 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