15

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?

Justin
  • 1,226
  • 4
  • 18
  • 21
  • Presumably another process will import the Task subclasses and invoke the run() method. Not sure if that answers your question? – Justin Sep 17 '13 at 19:59
  • Why don't you just have them override `run`, but have the API be to call some different method which does the before/after stuff and calls `run` itself? – BrenBarn Sep 17 '13 at 20:00
  • Why not just have the `run()` from `Task` do what it has to do and call a function that is overriden in the derived class such as `do_run()`? You are still only asking the derived class to implement one function. – Rod Sep 17 '13 at 20:00
  • Yeah, those are all definitely viable options for my use case. I was mostly wondering if there was some decorator magic or if there was a standard way this type of thing is handled. – Justin Sep 17 '13 at 20:03

1 Answers1

20

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()
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • Thanks for the metaclass magic tip, that's what I was really wondering about. It looks like I'll probably have others override a hook, but it's really helpful to know that metaclasses are an option. Thanks! – Justin Sep 17 '13 at 20:21
  • I've always seen `__new__` used in metaclasses and not `__init__` - this comes out much cleaner. – Eric Sep 17 '13 at 20:45
  • The reason why `__new__` exists in Python is so that immutable types can preserve their immutability while allowing subclassing. (If there were no `__new__` and immutables used `__init__` then you could change the value of immutables by calling things like `math.pi.__init__(3.0)`!) So I believe you should reserve `__new__` only for those situations where you want the instances to be immutable. – unutbu Sep 17 '13 at 21:33
  • The metaclass example is exactly what I'm looking for but it doesn't seem perform as intended. I'm not seeing is print `before` or `after`. What am I missing? – Jørgen Sep 05 '17 at 15:38
  • 2
    @Jørgen: The syntax for declaring a metaclass changed between Python2 and Python3. If you are using Python3, add `metaclass=MetaTask` to the class base declaration. I've edited the post above to show what I mean. – unutbu Sep 05 '17 at 15:46
  • @unutbu: Thanks for quick response! – Jørgen Sep 05 '17 at 15:55