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.

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'.

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.

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.