I find myself frequently running into this sort of problem. I have a function like
def compute(input):
result = two_hour_computation(input)
result = post_processing(result)
return result
and post_processing(result) fails. Now the obvious thing to do is to change the function to
import pickle
def compute(input):
result = two_hour_computation(input)
pickle.dump(result, open('intermediate_result.pickle', 'wb'))
result = post_processing(result)
return result
but I don't usually remember to write all my functions that way. What I wish I had was a decorator like:
@return_intermediate_results_if_something_goes_wrong
def compute(input):
result = two_hour_computation(input)
result = post_processing(result)
return result
Does something like that exist? I can't find it on google.
The "outside" of a function has no access to the state of local variables inside the function at runtime whatsoever. So this cannot be solved with a decorator.
In any case, I would argue that the responsibility for catching errors and saving valuable intermediary results should be done explicitly by the programmer. If you "forget" to do that, it must have not been that important to you.
That being said, situations like "do X in case either A, B, or C raises an exception" are a typical use case for context managers. You can write your own context manager that acts as a bucket for your intermediary result (in place of a variable) and performs some
saveaction in case an exception exits it.Something like this:
Obviously, instead of
print(f"saved {self.value}!")insidesaveyou would do something like this:Now all you need to remember is to wrap those actions in a
with-statement and assign intermediary results to thevalueproperty of your context manager. To demonstrate:The output:
As you can see, the intermediary computed value
2.0was "saved", even though the next function raised an exception.It is worth noting that in this example, the context manager calls
saveonly if an exception was encountered, not if the context is exited "peacefully". If you wanted, you could make this unconditional of course.This may be not as convenient as just slapping a decorator onto a function, but it gets the job done. And IMO the fact that you have to still consciously wrap your important actions in this context is a good thing because it teaches you to pay special attention to these things.
This is the typical approach of implementing things like database transactions in Python by the way (e.g. in SQLAlchemy).
PS
To be fair, I should probably qualify my initial statement a bit. You could of course just use non-local state in your function, even though that is generally discouraged for good reason. In super simple terms, if in your example
resultwas a global variable (and you statedglobal resultinside the function), this could in fact be solved by a decorator. But I would not recommend that approach because global state is an anti-pattern. (And it would still require you to remember to use whatever global variable you designated for that job every time.)