pythonmethodspolymorphismdecorator

In python, how can/should decorators be used to implement function polymorphism?


Supposing we have a class as follows:

class PersonalChef():
    def cook():
        print("cooking something...")

And we want what it cooks to be a function of the time of day, we could do something like this:

class PersonalChef():
    def cook(time_of_day):
        ## a few ways to do this, but this is quite concise:
        meal = {'morning':'breakfast', 'midday':'lunch', 'evening':'dinner'}[time_of_day]
        print("Cooking", meal)

PersonalChef().cook('morning')
>>> Cooking breakfast

A potentially nice syntax form for this would be using decorators. With some under-the-hood machinery buried inside at_time, it ought to be possible to get it to work like this:

class PersonalChef():
    @at_time('morning')
    def cook():
        print("Cooking breakfast")

    @at_time('midday')
    def cook():
        print("Cooking lunch")

    @at_time('evening')
    def cook():
        print("Cooking dinner")

PersonalChef().cook('morning')
>>> Cooking breakfast

The reason this could be a nice syntax form is shown by how it then shows up in subclasses:

class PersonalChefUK(PersonalChef):
    @at_time('evening')
    def cook():
        print("Cooking supper")

The code written at the sub-class level is extremely minimal and doesn't require any awareness of the base-class implementation/data structures and doesn't require any calls to super() to pass-through the other scenarios. So it could be nice in a situation where there are a large number of people writing derived-classes for whom we want to pack and hide complexity away in the base class and make it hard for them to break the functionality.

However, I've tried a few different ways of implementing this and gotten stuck. I'm quite new to decorators, though, so probably missing something important. Any suggestions/comments?


Solution

  • A simpler approach would be to create a decorator factory that stores the decorated function in a dict that maps the function name and time of day to the function object, and returns a wrapper function that calls the stored function in the first class in the MRO that has it defined for the given time of day:

    def at_time(time_of_day, _actions={}):
        def decorator(func):
            def wrapper(self, time_of_day):
                for cls in type(self).__mro__:
                    if func := _actions.get((f'{cls.__qualname__}.{name}', time_of_day)):
                        return func(self)
                raise ValueError(f'No {name} found in the {time_of_day} time')
            name = func.__name__
            _actions[func.__qualname__, time_of_day] = func
            return wrapper
        return decorator
    

    so that:

    class PersonalChef():
        @at_time('morning')
        def cook(self):
            print("Cooking breakfast")
    
        @at_time('evening')
        def cook(self):
            print("Cooking dinner")
    
    class PersonalChefUK(PersonalChef):
        @at_time('evening')
        def cook(self):
            print("Cooking supper")
    
    PersonalChef().cook('morning')
    PersonalChef().cook('evening')
    PersonalChefUK().cook('morning')
    PersonalChefUK().cook('evening')
    

    outputs:

    Cooking breakfast
    Cooking dinner
    Cooking breakfast
    Cooking supper
    

    Demo: https://ideone.com/9T94sq