Farm Development

multiple inheritance woes

multiple inheritance in python starts to fall apart when you want to mash together two very similar objects. I haven't found a clean way to get this example to work (without hardcoding the super calls to self.rollback_db() or doubling up implementations of rollback_db, both of which seem like they shouldn't be necessary).

Does anyone have a suggestion? Aside from this gotcha, I often find multiple inheritance to be an elegant solution.

"""
>>> rolledback = set()
>>> class A(object):
...     def rollback(self):
...         self.rollback_db()
...         
...     def rollback_db(self):
...         rolledback.add('A')
... 
>>> class B(object):
...     def rollback(self):
...         self.rollback_db()
...         
...     def rollback_db(self):
...         rolledback.add('B')
... 
>>> class C(A, B):
...     def rollback(self):
...         A.rollback(self)
...         B.rollback(self)
... 
>>> c = C()
>>> c.rollback()
>>> rolledback
set(['A', 'B'])
"""
import doctest
doctest.testmod()

output is...

kumar$ python test_mro.py 
**********************************************************************
File "test_mro.py", line 24, in __main__
Failed example:
    rolledback
Expected:
    set(['A', 'B'])
Got:
    set(['A'])
**********************************************************************

and, yes, this was not a fun one to debug :( Thankfully this was discovered in a functional test of --dry-run!

UPDATE

There are some helpful suggestions in the comments, but I still don't see a solution! Here is an example fixed a little bit using super(), but still only 50% there:

"""
>>> rolledback = []
>>> class Rollbackable(object):
...     def rollback(self):
...         pass
... 
>>> class A(Rollbackable):
...     def rollback(self):
...         super(A, self).rollback()
...         rolledback.append('A')
...         self.rollback_db()
...         
...     def rollback_db(self):
...         rolledback.append('db(A)')
... 
>>> class B(Rollbackable):
...     def rollback(self):
...         super(B, self).rollback()
...         rolledback.append('B')
...         self.rollback_db()
...         
...     def rollback_db(self):
...         rolledback.append('db(B)')
... 
>>> class C(A, B):
...     def rollback(self):
...         super(C, self).rollback()
... 
>>> c = C()
>>> c.rollback()
>>> rolledback
['B', 'db(B)', 'A', 'db(A)']
"""
import doctest
doctest.testmod()

... and the output...

kumar$ python test_mro.py 
**********************************************************************
File "test_mro.py", line 31, in __main__
Failed example:
    rolledback
Expected:
    ['B', 'db(B)', 'A', 'db(A)']
Got:
    ['B', 'db(A)', 'A', 'db(A)']
**********************************************************************
1 items had failures:
   1 of   8 in __main__
***Test Failed*** 1 failures.

UPDATE #2

Thanks for all the helpful comments. It appears that the only way to accomplish this in python is to actually change the self.rollback_db() to a private method, via python magic underscores, like so: self.__rollback_db(). The downside to this of couse is that no subclass can ever override __rollback_db(). This can be inflexible at times. For example, I've wanted to override private methods before but couldn't (coincidentally, when trying to extend doctest; Instead I had to copy/paste about 20 lines of code). So use private methods only if you absolutely have to. In this case it makes that rollback_db() is private since it only rolls back a single db transaction and that's it. Also take note that super doesn't need to be called from class C since everything just works itself out (because of super in the base classes).

Now ... here is what passes the doctest!

"""
>>> rolledback = []
>>> class Rollbackable(object):
...     def rollback(self):
...         pass
... 
>>> class A(Rollbackable):
...     def rollback(self):
...         super(A, self).rollback()
...         rolledback.append('A')
...         self.__rollback_db()
...         
...     def __rollback_db(self):
...         rolledback.append('db(A)')
... 
>>> class B(Rollbackable):
...     def rollback(self):
...         super(B, self).rollback()
...         rolledback.append('B')
...         self.__rollback_db()
...         
...     def __rollback_db(self):
...         rolledback.append('db(B)')
... 
>>> class C(A, B):
...     pass
... 
>>> c = C()
>>> c.rollback()
>>> rolledback
['B', 'db(B)', 'A', 'db(A)']
"""
import doctest
doctest.testmod()
  • Re: multiple inheritance woes

    super is your friend!

    >>> class C(A, B):

    ... def rollback(self):

    ... super(C, self).rollback()

    ...

    That will fix your problems!

  • Re: multiple inheritance woes

    huh. I changed the above mash-up of the two rollback() methods to be one, super(C, self).rollback() ... but it still gave me the same behavior as before. Can you post an example that passes the test above? I always thought super() only gives you the first method discovered by the MRO

  • Re: multiple inheritance woes

    *All* of your methods need to call super(), including A and B.

    This gets a little tricky when you have true multiple inheritance (like with mixins say), where there's no clear ancestor (ie, base class). In your case, the __mro__ is (C, A, B). If B tries to do super(B, self).rollback(), you'll get an exception b/c object doesn't have a rollback method.

    The solution is to define an "abstract base class" (to abuse a C++ term) that has a rollback() method that does nothing (literally def rollback(): pass) and have A & B inherit from it.

  • Re: multiple inheritance woes

    If possible moving the 'rollback_db' inside the 'rollback' method solves it:

    def rollback(self):

    A.rollback_db(self)

    B.rollback_db(self)

    Making the 'rollback_db' obfuscated^Wprivate is another solution:

    class A(object):

    def rollback(self):

    self.__rollback_db()

    And maybe you could use delegation:

    class C(object):

    def __init__(self, a, b):

    self.a = a

    self.b = b

    def rollback(self):

    self.a.rollback()

    self.b.rollback()

  • Re: multiple inheritance woes

    Thanks for all the suggestions, but stopping short of calling rollback_db() directly, I still don't see a solution. Is this just not possible? The super() does something I didn't think it did, it allows me to call both A.rollback and B.rollback (cool!) ... however, the reference to self in the subclasses are still wrong. I posted an updated example in case anyone has a fix.

  • Re: multiple inheritance woes

    Make rollback_db() private in your updated version, i. e. rename it to __rollback_db().

  • Re: multiple inheritance woes

    Thanks for all the comments. Looks like a private method it is. I updated the post with the working code.

  • Re: multiple inheritance woes

    I don't know if it's a smart idea, anyway. What you want to archieve is a particular class tied execution flow. That multiple-inheritance doesn't work the way you'd like it to can't be changed. But that doesn't mean you can't archieve the desired flow of control in a very similar fashion in python. So here goes, an alternative way to get there not using python's multiple inheritance at all.

    http://paste.pocoo.org/show/1301/

  • Re: multiple inheritance woes

    interesting. This makes it so you don't have to create private methods, but there is a problem: if you want to call a super method you have to explicitly call it. The nice thing about the above solution is that super methods are called naturally (due to the MRO). I think that might make it a better solution, IMHO.

    Although, what you may be getting at, and what someone else mentioned, is that delgation (over inheritance) may be a better idea. In my particular application delegation is used more than inheritance. There is [more or less] a command that has a stack of actions; each action is called sequentially.

    But in the case of an action that uses two database connections (the above scenario), I think it makes more sense to use inheritance over delegation. The fact that two connections are needed is a detail of the action, not the command.

  • Re: multiple inheritance woes

    Another pretty obvious solution without private methods:

    http://paste.pocoo.org/show/2178/

  • Re: multiple inheritance woes

    I've been having a similar problem and I ended up doing something like (per your example):

    replacing (in class A):

    self.rollback_db()

    with:

    A.rollback_db(self)

    Don't know if this was your exact problem, but it seems to work for me. It's annoying but I guess if a class calls one of its member functions you should avoid self.func() and use class.func(self).

Note: HTML tags will be stripped. Hit enter twice for a new paragraph.

Recent Projects

  • JSTestNet

    Like botnet but for JS tests in CI.

  • Nose Nicedots

    Nose plugin that prints nicer dots.

  • Fudge

    Mock objects for testing.

  • Fixture

    Loading and referencing test data.

  • NoseJS

    Nose plugin that runs JavaScript tests for a Python project.

  • Wikir

    converts reST to various Wiki formats.