0

My Python project performs a complex set of operations, and it's important to make clear which operations it uses and in what order. Therefore, it has a main method that reads like an overview of the project, with the implementation of operations outside the main method.:

class Program:
 def main(self): <--- This should be easy to read
 res1 = self.operation1()
 res2 = self.operation2(res1)
 if cond(res2):
 res3 = self.operation3(res2)
 .......

I also need to output many graphs based on the operations' results - both intermediary and final. This is mostly internal, for deubgging purposes. Since the graphs at later stages depend on the results of previous stages, I needed some container to hold the results over the code run. However, adding the output handling to the main module really cluttered it.
As a solution, I've wrapped the instance methods in the Program class with decorators, where the decoratos are implemented in an output module. Here's how it looks:

----- main.py -------
class Program:
 @main_wrap
 def main(self):
 .......
 @wrap1
 def operation1(self):
 .......
 return res
----- output.py -------
def main_wrap(main):
 def inner(self):
 self.res_container = [] # <-- creating instance variable outside __init__
 main(self, *args, **kwargs)
 output_final_results(self.res_container)
 return inner
def wrap1(func1):
 def inner(self, *args, **kwargs):
 res = func1(self, *args, **kwargs)
 self.res_container.append(res)
 output_intermediary_results(res)
 return inner

This approach feels very hack-y. I'd appreciate if you could comment or suggest a better solution that ensures both readability of the main method and proper code writing.

asked Jul 23, 2021 at 8:32

2 Answers 2

4

I would suggest you use dependency injection patterns for this and factor out the recording logic to its own class(es).


class Program:
 def __init__(self, recorder):
 self._recorder = recorder
 def operation1(self):
 .......
 self._recorder.log(res)
 return res
 ....

Then you can define two classes for recording

class NoOpRecorder:
 def log(self, *args):
 pass
class DebugRecorder:
 def __init__(self):
 self._results = []
 def log(self, *args):
 self._results.append(args)

So when running your program you can decide


if __name__ == "__main__":
 if debug_run:
 recorder = DebugRecorder()
 else:
 recorder = NoOpRecorder()
 Program(recorder)

Side note: using Dependency Injection (DI), your Program class turns into the 'aggregate root'.

answered Jul 23, 2021 at 10:49
2
  • I favour this solution. It makes it more explicit what's going on than in the decorated version (which reminds me a bit of AOP) Commented Jul 23, 2021 at 12:32
  • 1
    Aggregate root is typically associated with Domain Driven Design. You might be thinking of composition root instead. Commented Jul 23, 2021 at 15:03
0

If the answer is really that complicated, maybe it's worth reifying the results into their own (probably immutable) object?

Your main() could then be for gathering the results to pass to the Result constructor:

class Result:
 ...
class Program:
 def main(self):
 res1 = self.operation1()
 res2 = self.operation2(res1)
 if cond(res2):
 res3 = self.operation3(res2)
 return Result(res1, res2, res3 or None)

It might also allow for some of the operations/computations to be a bit more 'lazy' and only get done if they're called.

answered Jul 23, 2021 at 15:21

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.