2

I am having the following Python problem:

I have a class with different attributes that depend on each other. If an attribute is changed on runtime, some attributes should change as well. I am aware of getters and the @property decorator.

However, in this particular case, there are attributes that will take a long time to be computed, because they involve solving some integrals numerically. I would like not to recompute them on each call as this would make the code very inefficient, but rather detect changes of the other attributes and only recompute if needed.

The code looks like this. For simplicity, I only have two attributes here.

class MyClass

    def __init__(self, a):
        self.a = a
        self.b = None
    
    @property
    def b():
        if b is not None:
            return b
        else:
            self.compute_b() # takes a long time.
            return b

    def compute_b(self):
        # compute b using self.a
        self.b = b
        

So what I want to do is detect changes of certain attributes and then reset the dependent attribute to None and only recompute them when needed.

I came up with a few ideas, but I am not happy with any of them. I will use again a and b as names for the respective attributes.

Use @property

Recompute b each time it is called. This is conceptionally the easiest solution, however also the slowest.

Write a setter(a) for a

I could do this. However, some attributes are inherited from other classes. So I would have to write setters in the derived class which seems a bit odd. I feel like this makes the code much less readable and it might behave in unexpected ways.

Overload __setattr__

I could overload __setattr__ in my class and reset b inside this special method if a has changed. I have, however, read that this is not good practice. E.g. if attributes are initialized in the wrong order I might reset them, even though it's not needed.

Make a copy of a when b is computed.

Make a copy of a when b is computed. Before returning a precomputed b check if a has changed. Basically setting a flag when computing b.

Use @functools.lru_cache

[https://docs.python.org/3/library/functools.html#functools.lru_cache]

This looks also quite promising, however, it seems like I have to pass arguments to my methods for it to work and I would like not to do that as there are many attributes that are needed. Otherwise, it is pointless to use a class in the first place.

I feel like I could make any of these ideas work, but none of them seem to be optimal.

Does anyone know how to deal with this problem in a cleaner fashion?

  • 1
    so... you're basically implementing a spreadsheet and need to find the best algorithm to refresh cells after a change - except you want to refresh the cells lazily...? If so you'll need to (i) wrap the variables in such a way that you can define dependencies, (ii) recursively mark all dependent cells as dirty when changing a cell value - possibly detecting loops, and (iii) recalculate the cell value before returning it if the cell is marked dirty. I don't think there is any direct language support for it, but it's not that difficult to build... – thebjorn Oct 13 '20 at 15:11
  • 1
    The observer pattern could be used here. – Wups Oct 13 '20 at 15:25
  • @thebjorn Thanks, I think your approach works as well. The problem is that the code is rather large and as mentioned above some attributes spread across multiple classes as they are inherited. I am mostly concerned with readability as the code will be used by some other people as well. Maybe introducing all these flags marking cells as dirty will lead to an even more complex code that I'd like to avoid. – FrankHauser Oct 13 '20 at 19:41
  • @Wups I will look into that thanks! – FrankHauser Oct 13 '20 at 19:41
  • @FrankHauser the observer pattern would be eager though, not lazy, or am I missing something? – thebjorn Oct 13 '20 at 20:43
  • @thebjorn I have looked into it now. It depends if I just reset the variable on a detected change and then only recompute it when needed or if I recompute them each time a change is detected. But either way, it's not exactly what I am looking for. I don't think it will make the code easier read. – FrankHauser Oct 15 '20 at 12:01

0 Answers0