Daniel Watson
Daniel Watson

Reputation: 473

Controlling child class method from parent class

Let's say I have this code:

class Foo:
    def write(self, s=""):

        # Make sure that overwritten
        # 'write' method in child class
        # does what it's specified, and
        # then what comes next...

        print "-From Foo"

class Bar(Foo):
    def write(self, s=""):
        print s

baz = Bar()
baz.write("Hello, World!")

The last call obviously outputs the hello world by itself. I need to make it write the "-From Foo" as well, but without modifying the Bar class, just the Foo class. I've tried using the __bases__ and other stuff, but it won't work for my purpose.

Upvotes: 4

Views: 360

Answers (4)

martineau
martineau

Reputation: 123443

Here's another way to do it using a metaclass. An important advantage it has over using __getattribute__()is that there's no additional overhead incurred for accessing or using other subclass attributes and methods. It also supports single-inheritance if subclasses of it are defined.

class Foo(object):
    class __metaclass__(type):
        def __new__(metaclass, classname, bases, classdict):
            clsobj = super(metaclass, metaclass).__new__(metaclass, classname, 
                                                         bases, classdict)
            if classname != 'Foo' and 'write' in classdict:  # subclass?
                def call_base_write_after(self, *args, **kwargs):
                    classdict['write'](self, *args, **kwargs)
                    Foo.write(self, *args, **kwargs)

                setattr(clsobj, 'write', call_base_write_after)  # replace method

            return clsobj

    def write(self, s=""):
        print "-From Foo"

class Bar(Foo):
    def write(self, s=""):
        print 'Bar:', s

class Baz(Bar):  # sub-subclass
    def write(self, s=""):
        print 'Baz:', s

Bar().write('test')
Baz().write('test')

Output:

Bar: test
-From Foo
Baz: test
-From Foo

If you would like sub-subclasswrite()methods to call their base class's version afterwards instead of the root (Foo) class's, just change the hardcoded:

    Foo.write(self, *args, **kwargs)

call to:

    super(clsobj, self).write(*args, **kwargs)

inFoo.__new__().

Upvotes: 2

Gareth Latty
Gareth Latty

Reputation: 88977

There is no (good) way to do this without modifying Bar() - what you want to do is use super() inside Bar(), this will allow you to call the parent method.

If you are using a class that you can't modify that doesn't do this, the best solution is to make a wrapper class that does what you want manually, using the class which isn't playing nice. E.g:

class BarWrapper(Foo):
    def __init__(self, *args, **kwargs):
        self.bar = Bar(*args, **kwargs)

    def write(self, *args, **kwargs):
        super(BarWrapper, self).write(*args, **kwargs)
        self.bar.write(*args, **kwargs)

(Naturally, more will be needed depending on how much there is to your class, and note in 3.x you can use the simpler syntax for super() by dropping the arguments.)

Upvotes: 3

Lie Ryan
Lie Ryan

Reputation: 64837

This is one way to do it using metaclass magic; IMHO, it's more robust and flexible than other approaches, it also handles unbounded call (e.g. Bar.write(x, "hello")) and single inheritance properly (see Baz below):

class ReverserMetaclass(type):
    def __new__(cls, name, bases, dct):
        """ This metaclass replaces methods of classes made from it
            with a version that first calls their base classes
        """
        # create a new namespace for the new class
        new_dct = {}
        for member_name, member in dct.items():
            # only decorate methods/callable in the new class
            if callable(member):
                member = cls.wrap(bases, member_name, member)
            new_dct[member_name] = member
        # construct the class
        return super(ReverserMetaclass, cls).__new__(cls, name, bases, new_dct)

        # instead of the above, you can also use something much simpler
        #     dct['read'] = cls.wrap(bases, 'read', dct['read'])
        #     return super(ReverserMetaclass, cls).__new__(cls, name, bases, dct)
        # if you have a specific method that you want to wrap and want to 
        # leave the rest alone

    @classmethod
    def wrap(cls, bases, name, method):
        """ this method calls methods in the bases before calling the method """
        def _method(*args, **kwargs):
            for base in bases:
                if hasattr(base, name):
                    getattr(base, name)(*args, **kwargs)
            # put this above the loop if you want to reverse the call order
            ret = method(*args, **kwargs)
            return ret
        return _method

A sample console run:

>>> class Foo(object):
...     __metaclass__ = ReverserMetaclass
...     def write(self, s=""):
...         # Make sure that overwritten
...         # 'write' method in child class
...         # does what it's specified, and
...         # then what comes next...
...         print "Write - From Foo", s
...     def read(self):
...         print "Read - From Foo"
...
>>> class Bar(Foo):
...     def write(self, s=""):
...         print "Write - from Bar", s
...     def read(self):
...         print "Read - From Bar"
...
>>> class Baz(Bar):
...     def write(self, s=""):
...         print "Write - from Baz", s
...
>>> x = Bar()
>>> x.write("hello")
Write - From Foo hello
Write - from Bar hello
>>> Bar.read(x)
Read - From Foo
Read - From Bar
>>>
>>> x = Baz()
>>> x.read()
Read - From Foo
Read - From Bar
>>> x.write("foo")
Write - From Foo foo
Write - from Bar foo
Write - from Baz foo

Python metaclass is extremely powerful, though as others have said, you really do not want to do this kind of magic too often.

Upvotes: 4

Bakuriu
Bakuriu

Reputation: 101919

I 100% agree with Lattyware: you shouldn't do this. Parent classes shouldn't "know" about subclasses or how they work.

But I must say that it is possible using some __getattribute__ magic:

class Foo(object):
    def __getattribute__(self, attr):
        if attr != 'write':
            return super(Foo, self).__getattribute__(attr)
        meth = super(Foo, self).__getattribute__(attr)
        if meth.im_func is Foo.write.im_func:
            # subclass does not override the method
            return meth

        def assure_calls_base_class(*args, **kwargs):
            meth(*args, **kwargs)
            Foo.write(self, *args, **kwargs)
        return assure_calls_base_class

    def write(self, s=""):
        print "-From Foo"



class Bar(Foo):
    def write(self, s=""):
        print s

Running the code:

>>> b = Bar()
>>> b.write('Hello, World!')
Hello, World!
-From Foo

Note however that this is simply an hack, and will probably break when using a bit of inheritance, or even if you access write from the class:

>>> Bar.write(b, 'Hello, World!')  #should be equivalent to b.write('Hello, World!')
Hello, World!

Upvotes: 4

Related Questions