Multiple Inheritance and super() (was Re: [Merge] lp:~vila/bzr/389648-hook-calls-base into lp:~bzr/bzr/trunk)
John Arbash Meinel
john at arbash-meinel.com
Sat Nov 7 20:20:59 GMT 2009
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
Vincent Ladeuil wrote:
...
> jam> I should comment that using "super()" is not always
> jam> correct anymore.
>
>
> jam> To start with, it only matters when you have multiple
> jam> inheritance. But even more importantly, python 2.6
> jam> 'broke' super(...).__init__() if there are any
> jam> arguments.
>
> I disagree. python-2.6 is just stricter and catch bogus calls.
>
> jam> Because object.__init__() no longer allows
> jam> arguments. (So as near as I can tell, there is no way to
> jam> safely __init__ a multiple inheritance structure that
> jam> wants arguments passed as part of init.
>
> Yes there is, you handle arguments where they are expected and
> you don't pass them where they are not.
So I'll expand a bit more on this later. But if you are going to "pass
them where they are expected, and not where they are not", then you
can't use super() [because what that actually represents is not static,
but based on the inheritance heirarchy]. And with python 2.6 you can't
use **kwargs to allow any arguments to __init__ and have them passed to
whoever actually cares about them.
>
> The cases I had to fix when I addressed python-2.6 compatibility
> weren't that complex once I understood what was going on.
>
> And as I remember the only case that seemed controversial at the
> time was indeed an abuse.
So imagine the case:
class Mixin1(object):
def __init__(self, keyword1=None):
self.keyword1 = keyword1
class Mixin2(object):
def __init__(self, keyword2=None):
self.keyword2 = keyword2
class Mixed(Mixin1, Mixin2):
def __init__(self, keyword1=None, keyword2=None):
?
In python2.5 you could have written this as:
class Mixin1(object):
def __init__(self, keyword1=None, **kwargs):
super(Mixin1, self).__init__(**kwargs)
self.keyword1 = keyword1
class Mixin2(object):
def __init__(self, keyword2=None, **kwargs):
super(Mixin2, self).__init__(**kwargs)
self.keyword2 = keyword2
class Mixed(Mixin1, Mixin2):
def __init__(self, keyword1=None, keyword2=None, **kwargs):
super(Mixed, self).__init__(keyword1=keyword1, keyword2=keyword2,
**kwargs)
And it would have worked, and the parameters would have been passed to
the appropriate base class.
In Python2.6 this fails because object.__init__() doesn't take any
parameters. So instead Mixed has to be:
class Mixin1(object):
def __init__(self, keyword1=None):
self.keyword1 = keyword1
class Mixin2(object):
def __init__(self, keyword2=None):
self.keyword2 = keyword2
class Mixed(Mixin1, Mixin2):
def __init__(self, keyword1=None, keyword2=None):
Mixin1.__init__(self, keyword1=keyword1)
Mixin2.__init__(self, keyword2=keyword2)
But note that this is explicitly *not* done using super().
Now *if* you didn't plan ahead for multi-inheritance by allowing
**kwargs, then things break. And you can work around this by doing:
class KwargsInit(object):
def __init__(self, *args, **kwargs):
# Don't call super() here, it breaks things in python2.6
pass
class Mixin1(KwargsInit):
def __init__(self, keyword1=None, **kwargs):
super(Mixin1, self).__init__(**kwargs)
self.keyword1 = keyword1
class Mixin2(KwargsInit):
def __init__(self, keyword2=None, **kwargs):
super(Mixin2, self).__init__(**kwargs)
self.keyword2 = keyword2
class Mixed(Mixin1, Mixin2):
def __init__(self, keyword1=None, keyword2=None, **kwargs):
super(Mixed, self).__init__(keyword1=keyword1, keyword2=keyword2,
**kwargs)
That works because the diamond is rooted at KwargsInit, and not
'object'. However, all of your classes in your multi-inheritance
hierarchy that want to use **kwargs style initialization have to inherit
from KwargsInit and *not* object.
Anyway, this is a separate discussion, so I moved it to the regular list.
> If you can provide examples in the actual code base where calling
> super().__init__ will be bogus, I'll be happy to revise my
> opinion.
>
> Vincent
Only that in the actual code base we've side-stepped the issue by not
really using multiple inheritance, and not passing arguments via
__init__. (We *do* pass arguments via __init__ but only in cases where
we aren't using multiple inheritance, which are cases where super()
provides *no* benefit over just calling BaseClass.__init__(self, ...))
I agree it would be good to come up with a reasonable standard across
the code base. I think that
a) super() is meant to help in the multi-inheritance case, otherwise it
is just the same as directly calling BaseClass.__init__().
b) You can't pass arguments to super().__init__ if there is a chance to
actually get multiple inheritance. In the case above, if Mixin1 didn't
call super() then Mixin2.__init__ would not get called.
How about a different structure.
class Mixin1(object):
def __init__(self, verbose):
self.verbose = True
if verbose:
print "I'm Mixin1 in verbose mode"
class Mixin2(object):
def __init__(self, verbose):
self.verbose = True
if verbose:
print "I'm Mixin2 in verbose mode"
class Mixed(Mixin1, Mixin2):
def __init__(self, verbose):
???
If you just use super as:
class Mixed(Mixin1, Mixin2):
def __init__(self, verbose):
super(Mixed, self).__init__(verbose)
Then Mixin2.__init__ never gets called. You need to change Mixin1 to:
class Mixin1(object):
def __init__(self, verbose):
super(Mixin1, self).__init__(verbose)
self.verbose = True
if verbose:
print "I'm Mixin1 in verbose mode"
however, if you *do* that, it breaks in python2.6 because
object.__init__ doesn't allow arguments to be passed.
You basically have to create a new AbstractBaseClass for
multiple-inheritance which replaces 'object' as the root of your
diamond, and *doesn't* call super().__init__() itself.
I suppose this is consistent with the behavior for non-__init__ functions:
class Mixin1(object):
def do_foo(self, arg):
super(Mixin1, self).do_foo(arg)
print "I'm Mixin1: ", arg
class Mixin2(object):
def do_foo(self, arg):
super(Mixin2, self).do_foo(arg)
print "I'm Mixin2: ", arg
class Mixed(Mixin1, Mixin2):
def do_foo(self, arg):
super(Mixed, self).do_foo(arg)
print "I'm Mixed: ", arg
m = Mixed()
m.do_foo(1)
Fails because eventually super() finds a class that doesn't have a
do_foo attribute.
Replacing the above with:
class Mixers(object):
def do_foo(self, arg):
print "Mixers doesn't call super()"
class Mixin1(Mixers):
def do_foo(self, arg):
super(Mixin1, self).do_foo(arg)
print "I'm Mixin1: ", arg
class Mixin2(Mixers):
def do_foo(self, arg):
super(Mixin2, self).do_foo(arg)
print "I'm Mixin2: ", arg
class Mixed(Mixin1, Mixin2):
def do_foo(self, arg):
super(Mixed, self).do_foo(arg)
print "I'm Mixed: ", arg
Works and gives:
>>> m.do_foo(1)
Mixers doesn't call super()
I'm Mixin2: 1
I'm Mixin1: 1
I'm Mixed: 1
John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/
iEYEARECAAYFAkr11qsACgkQJdeBCYSNAAMb4QCeK5LNFe1xGOql+9uzsfnAysdf
VqMAoI0n+TocTYZF7PIBjGyFmjXxX/Ja
=U8CQ
-----END PGP SIGNATURE-----
More information about the bazaar
mailing list