Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python: Decorating a class method that is intended to be overwritten when inherited

Let's say I have some base class:

class Task:
    def run(self):
        #override this!

Now, I want others to subclass Task and override the run() method:

class MyTask(Task):
    def run(self):
        #successful override!

However, the problem is that there is logic that must take place before and after the run() method of every class that subclasses Task.

It seems like one way I could do this would be to define another method in the base class which then calls the run() method. However, I wanted to ask, is there a way to achieve this with decorators? What would be the most pythonic way of doing this?

like image 483
Justin Avatar asked Sep 17 '13 19:09

Justin


1 Answers

As suggested in the comments, letting the subclasses override a hook instead of run itself would probably be best:

class Task(object):
    def run(self):
        # before 
        self.do_run()
        # after

class MyTask(Task):
    def do_run(self):
        ...

task = MyTask()
task.run()

However, this is one way you could do it with a class decorator:

def decorate_run(cls):
    run = getattr(cls, 'run')
    def new_run(self):
        print('before')
        run(self)
        print('after')
    setattr(cls, 'run', new_run)
    return cls


class Task(object): pass

@decorate_run
class MyTask(Task):
    def run(self):
        pass

task = MyTask()
task.run()

# prints:
# before
# after

Another way would be to use a metaclass. The advantage of using a metaclass would be that subclasses wouldn't have to be decorated. Task could be made an instance of the metaclass, and then all subclasses of Task would inherit the metaclass automatically.

class MetaTask(type):
    def __init__(cls, name, bases, clsdict):
        if 'run' in clsdict:
            def new_run(self):
                print('before')
                clsdict['run'](self)
                print('after')
            setattr(cls, 'run', new_run)

class Task(object, metaclass=MetaTask):
    # For Python2: remove metaclass=MetaTask above and uncomment below:
    # __metaclass__ = MetaTask
    pass

class MyTask(Task):
    def run(self):
        #successful override!
        pass

task = MyTask()
task.run()
like image 119
unutbu Avatar answered Nov 13 '22 00:11

unutbu