farmdev

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()