ChrisCScott / forecaster

A personal finances forecasting tool for Canadian retirement planning
Other
1 stars 2 forks source link

Support replacement of `Scenario` objects in function closures #82

Open ChrisCScott opened 2 years ago

ChrisCScott commented 2 years ago

We ought to replace references to replacement Scenario objects (and probably any other replaced objects too) in function closures when copying objects in Forecaster.run_forecast.

This will probably require overloading deepcopy to recurse onto functions (which ordinarily it ignores and returns the original function) and onto their closures. You can replace the contents of a closure simply by invoking funcname.__closure__[index].cell_contents = new_value, but to avoid mutating the original closure you'll need to copy the original closure, which deepcopy does not support.

So we will need to:

  1. Overload deepcopy to copy and recurse onto functions
  2. Copy closures (e.g. by calling new_cell = cell(f.__closure__[index].cell_contents) for each index in the closure)
  3. Recurse onto cell contents

For resources on copying functions, see this answer.

ChrisCScott commented 2 years ago

deepcopy can't be overloaded in this way (it's a free function and doesn't provide any hooks for overriding its recursion).

The alternative is probably to perform deepcopy, and then iterate over copied objects and inspect them for function attributes that provide a __closure__ attribute.

Recursion will probably be required, since potentially closure vars could themselves reference closured objects. One option would be to recurse over a reference graph for an object, just like deepcopy (except that we'd be recursing onto functions too) - start with an object, collect its attributes, and recurse onto them, maintaining a memo of visited objects to avoid infinite recursion. This would probably require using a fresh memo, since we don't want to terminate recursion when visiting objects that were visited by the earlier run of deepcopy. Consider whether this means we'd need to have two memos, if we need to reference the one produced by deepcopy; preferably any analysis of the deepcopy could be avoided, since its documentation discourages this (although its implementation is well-known and straightforward.)

The other option would be to iterate over objects in deepcopy's memo, inspect their attributes for objects with __closure__ attributes, mutate those objects by replacing those functions with copies (with mutated closures), and use deepcopy to perform the copying of the closure vars. One point of complexity in this approach would be that we cannot mutate a dict while iterating over it, so we'd probably need to iterate over a copy and then [recursively?] repeat for any newly-added elements (can do this by finding the difference between keys in the original and mutated dicts; see here). This involves analyzing memo, but doesn't require any knowledge of what the keys are (any mutation of memo outside of deepcopy) - just the knowledge that the values of memo are the copies produced by deepcopy. deepcopy itself does the heavy lifting in terms of copying/mutating.