Caching that resets itself under certain circumstances: Reduce boiler plate code In my machine learning programs I have the typical situation where I need to perform certain operations on data, which results in the following graph:

data --op A--&gt; intermediate_state --op B--&gt; final result type 1
data --op A--&gt; intermediate_state --op C--&gt; final result type 2
data --op A--&gt; intermediate_state --op D--&gt; final result type 3

So in order to not have to repeat operation A all the time I provide a &quot;fit&quot; method, which implements operation A.
Then, I implement transformation methods that implement the other operations.
However, since I usually only need some of the final results in the graph leaves, I want to calculate those on the one hand lazily, but then cache the result.
Since the operations require a parameter, I provide this one in the constructor.
I also need to pass a parametrized object with the same parametrization to different parts of the the program, where the data of operation A changes, without the parametrization changing.
That is the reason why fit is it's own method and not part of the constructor.
Also, because of that I cannot simply cache the results of the operations that are not A, without resetting them after invoking a new fit.
Here is my result in Python:
class MyClass:       

    def __init__(self, parameter):
        self.parameter = parameter

    # expensive machine learning fitting
    def fit(self, data) -&gt; MyClass:
        self.intermediate_data_ = data + self.parameter + 2
        self._complex_transform1 = None
        self._complex_transform2 = None
        return self

    # expensive machine learning operation version 1
    @property
    def complex_transform1(self):
        if self._complex_transform1 is None:
            self._complex_transform1 = self.intermediate_data_ / self.parameter / 2
        return self._complex_transform1

    # expensive machine learning operation version 2
    @property
    def complex_transform2(self):
        if self._complex_transform2 is None:
            self._complex_transform2 = self.intermediate_data_ / self.parameter / 5
        return self._complex_transform2

The problem I have with this approach is that I am repeating quite a bit of boiler plate code.
(Note, that I have many more operations that are not A.)
Namely:

The variables self._complex_transform1 and self._complex_transform2

The constructs if self._complex_transform1 is None: could be reduced from
     if self._complex_transform1 is None:
         self._complex_transform1 = self.intermediate_data_ / self.parameter / 2
     return self._complex_transform1



to
        return self.intermediate_data_ / self.parameter / 2

and I would save a nesting.
I was thinking of defining a container class that contains the results and is reset by fit. But I do not see how to implement this without basically introducing the same about of boilerplate code into the transform methods.
Some decorator-approach might be great, but I am not sure how to approach this.
This might be a job for @functools.cached_property. It works a lot like @property, except it remembers the value between calls. The value can then be cleared using del in the fit function, or elsewhere if needed.
Unfortunately, the properties can't be deled until they've been set, which means we have to re-add the boiledplate in fit in the form of try: del x; except AttributeError: pass. Maybe that's an improvement since it's at least centralized, but it's still not ideal.
There are workarounds like
def fit(self, data):
    self.intermediate_data_ = ...

    for prop in [&quot;complex_transform1&quot;, &quot;complex_transform2&quot;]:
        try:
            delattr(self, prop)
        except AttributeError:
            pass

    return self

Or perhaps something like
def fit(self, data):
    self.intermediate_data_ = ...

    for prop in self.reset_on_fit:
        delattr(self, prop)

    self.reset_on_fit = []
    return self

with the properties looking like
@cached_property
def complex_transform1(self):
    self.reset_on_fit.append(&quot;complex_transform1&quot;)
    return self.intermediate_data_ / self.parameter / 2

But putting that data into a hardcoded string is not very refactor-friendly. Perhaps we can define a decorator to figure that out for us?
Well, I haven't tested it very thoroughly, but I think we can:
from functools import cached_property, wraps

def resets_on_fit(fn):
    @wraps(fn)
    def wrapper(self):
        self.reset_on_fit.append(fn.__name__)
        return fn(self)

    return wrapper

class MyClass:
    # ...

    def fit(self, data):
        self.intermediate_data_ = data + self.parameter + 2
        
        for prop in self.reset_on_fit:
            delattr(self, prop)
        
        self.reset_on_fit = []
        return self

    @cached_property
    @resets_on_fit
    def complex_transform1(self):
        return self.intermediate_data_ / self.parameter / 2