Search code examples
pythonoopdesign-patterns

Python design for calling functions with different signatures


Suppose I have a parent class P with a method main that implements the core functionality of the class. main itself calls a function defined in another module called parent_function, with signature parent_function(x, y).

e.g.

from my_module import parent_function

class P:
    def main():
        # do something
        x = P.calc_x()
        y = P.calc_y()
        z = P.calc_z()

        # execute `parent_function`
        parent_function(x,y)
        
        # do more things

Now suppose I have a child class C which differs from P by calling child_function, with signature child_function(x,y,z), instead of parent_function(x,y). To call child_function with its different signature, I have to rewrite the whole of the main method, changing just one line

e.g.

from my_module import child_function

class C(P):
    def main():
        # do something
        x = P.calc_x()
        y = P.calc_y()
        z = P.calc_z()

        # execute `child_function`
        child_function(x,y,z)

        # do more things

This is a violation of the DRY principle.

How can I redesign the code to avoid rewriting the whole main method?

Some ideas:

  • Turn parent_function, child_function into methods of P / C, set z as an instance variable so the signatures of the two methods can be the same. I dislike this solution as in my case it does not make sense for z to be an instance variable.

  • Store the output (x,y,z) into a dictionary, change the signature of parent/child_function to accept **kwargs, and ignore unused kwargs in parent_function.


Solution

  • Rather than changing the parent / child function (which may be library functions, which you can't change), you may create a wrapper for those calls in you class.

    def parent_function(x, y):
        print("parent", x, y)
    
    def child_function(x, y, z):
        print("child", x, y, z)
    
    
    class P:
    
        @staticmethod
        def calc_x():
            return 1
        
        @staticmethod
        def calc_y():
            return 2
        
        @staticmethod
        def calc_z():
            return 3
    
        @classmethod
        def internal_call(cls, args):
            parent_function(args[0], args[1])
    
        @classmethod
        def get_data(cls):
            x = P.calc_x()
            y = P.calc_y()
            return [x, y]
        
        @classmethod
        def main(cls):
            args = cls.get_data()
    
            cls.internal_call(args)
    
    class C(P):
    
        @classmethod
        def internal_call(cls, args):
            child_function(args[0], args[1], args[2])
    
        @classmethod
        def get_data(cls):
            x = P.calc_x()
            y = P.calc_y()
            z = P.calc_z()
            return [x, y, z]
        
    P.main()
    C.main()
    

    So that, you overwrite the way to get input data, and which function to forward it to, but not the overall logic of the main function.