Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

python: bookkeeping dependencies in cached attributes that might change

I have a class A with three attributes a,b,c, where a is calculated from b and c (but this is expensive). Moreover, attributes b and c are likely to change over times. I want to make sure that:

  1. a is cached once it is calculated and then reproduced from cache
  2. if b or c change then the next time a is needed it must be recomputed to reflect the change

the following code seems to work:

class A():

    def __init__(self, b, c):
        self._a = None
        self._b = b
        self._c = c

    @property
    def a(self):
        if is None:
            self.update_a()
        return self._a

    def update_a(self):
        """
        compute a from b and c
        """
        print('this is expensive')
        self._a = self.b + 2*self.c

    @property
    def b(self):
        return self._b

    @b.setter
    def b(self, value):
        self._b = value
        self._a = None #make sure a is recalculated before its next use

    @property
    def c(self):
        return self._c

    @c.setter
    def c(self, value):
        self._c = value
        self._a = None #make sure a is recalculated before its next use

however this approach does not seem very good for many reasons:

  1. the setters of b and c needs to know about a
  2. it becomes a mess to write and maintain if the dependency-tree grows larger
  3. it might not be apparent in the code of update_a what its dependencies are
  4. it leads to a lot of code duplication

Is there an abstract way to achieve this that does not require me to do all the bookkeeping myself? Ideally, I would like to have some sort of decorator which tells the property what its dependencies are so that all the bookkeeping happens under the hood.

I would like to write:

@cached_property_depends_on('b', 'c')
def a(self):
    return self.b+2*self.c

or something like that.

EDIT: I would prefer solutions that do not require that the values assigned to a,b,c be immutable. I am mostly interested in np.arrays and lists but I would like the code to be reusable in many different situations without having to worry about mutability issues.

like image 427
Tashi Walde Avatar asked Jan 15 '18 11:01

Tashi Walde


People also ask

How do you maintain cache in Python?

Implementing a Cache Using a Python Dictionary You can use the article's URL as the key and its content as the value. Save this code to a caching.py file, install the requests library, then run the script: $ pip install requests $ python caching.py Getting article... Fetching article from server...

What is cached property in Python?

cached_property is a decorator that converts a class method into a property whose value is calculated once and then cached like a regular attribute. The cached value will be available until the object or the instance of the class is destroyed.

How do you use the cache function in Python?

lru_cache basics To memoize a function in Python, we can use a utility supplied in Python's standard library—the functools. lru_cache decorator. Now, every time you run the decorated function, lru_cache will check for a cached result for the inputs provided. If the result is in the cache, lru_cache will return it.


1 Answers

You could use functools.lru_cache:

from functools import lru_cache
from operator import attrgetter

def cached_property_depends_on(*args):
    attrs = attrgetter(*args)
    def decorator(func):
        _cache = lru_cache(maxsize=None)(lambda self, _: func(self))
        def _with_tracked(self):
            return _cache(self, attrs(self))
        return property(_with_tracked, doc=func.__doc__)
    return decorator

The idea is to retrieve the values of tracked attributes each time the property is accessed, pass them to the memoizing callable, but ignore them during the actual call.

Given a minimal implementation of the class:

class A:

    def __init__(self, b, c):
        self._b = b
        self._c = c

    @property
    def b(self):
        return self._b

    @b.setter
    def b(self, value):
        self._b = value

    @property
    def c(self):
        return self._c

    @c.setter
    def c(self, value):
        self._c = value

    @cached_property_depends_on('b', 'c')
    def a(self):
        print('Recomputing a')
        return self.b + 2 * self.c
a = A(1, 1)
print(a.a)
print(a.a)
a.b = 3
print(a.a)
print(a.a)
a.c = 4
print(a.a)
print(a.a)

outputs

Recomputing a
3
3
Recomputing a
5
5
Recomputing a
11
11
like image 180
vaultah Avatar answered Sep 28 '22 14:09

vaultah