nCircle VERT Blog

super is as super does

The other day one of my co-workers (hi Bob!) reviewed a tricky bit of Python code that I had written and asked why I was explicitly calling <BaseClass>.<method> instead of using 'super'. And my immediate answer (to myself, since Bob had gone offline) was 'I never use super, super is bad'.

However, that comment got me thinking, why don't I use Python's built-in super? Well, I knew why, back when I was getting started with Python I read an article called "Python's Super Considered Harmful" (by James Knight) and made a mental note to myself to never use it. End of story, I never re-examined that decision and I suspect that I may have passed this 'wisdom' on to others.

So here it is two years and several thousand lines of Python later, I went back and re-read the article. With a more experienced eye I found a number of points that (in my opinion) were somewhat subjective conclusions about the merits and risks of super. I also found the following discussion relating to the article between the Mr. Knight and GvR (if you don't want to read the whole thread, the summary is GvR rebuts the assertion that super is in fact the problem). I also found an extensive description of when and how to correctly use super.

However, after all that reading, I had a new question that I didn't know the answer to: In a new style base class (e.g. one that inherits directly from object), is it necessary or beneficial to __init__ the parent class? For example:

class A( object ):
    def __init__( self, *args, **kwargs )
        super( A, self ).__init__( *args, **kwargs )   # Do I need to do this?

The official documentation is not clear (at least to me) on this issue. There are many examples of classes which do not make this call.

You'd think it would be explained here, but it's not really addressed. There is a discussion in this bug which suggests to me that it isn't something that is always addressed.

The best treatment of this issue that I have found is in the link I posted above, which states "Note that in the first case M's call to super is critical to allowing the rest of the init calls to happen, even though its superclass is object." But their example doesn't really state why it is critical, so I played with it for a bit. My first try was this:

class A( object ):
    def __init__( self, *args, **kwargs ):
        super( A, self ).__init__( *args, **kwargs)
        print 'A'
class B( A ):
    def __init__( self, *args, **kwargs ):
        super( B, self ).__init__( *args, **kwargs)
        print 'B'
class C( A ):
    def __init__( self, *args, **kwargs ):
        super( C, self ).__init__( *args, **kwargs)
        print 'C'
class D( B, C ):
    def __init__( self, *args, **kwargs ):
        super( D, self ).__init__( *args, **kwargs)
        print 'D'
>>> print D.__mro__
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <type 'object'> )
>>> d = D()
A
C
B
D

Okay... so what happens if A's call to super is commented out?

>>> print D.__mro__
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <type 'object'> )
>>> d = D()
A
C
B
D

Nothing! So it doesn't matter, right? Well... then why did the Chandler Project article say it was critical? Their inheritance example was a little more complex, involving two paths of inheritance from object:

class M(object):
  """ Mixin class """
  def __init__(self, *args, **kwds):
    print 'init.M'
    super(M, self).__init__(*args, **kwds)
class B(object):
  """ Base class """
  def __init__(self, *args, **kwds):
    print 'init.B'
    super(B, self).__init__(*args, **kwds)
class D(B):
  """ Derived class """
  def __init__(self, *args, **kwds):
    print 'init.D'
    super(D, self).__init__(*args, **kwds)
class CL(M, D):
  """ Class Left """
  def __init__(self, *args, **kwds):
    print 'init.CL'
    super(CL, self).__init__(*args, **kwds)
>>> print CL.__mro__
( <class '__main__.CL'>, <class '__main__.M'>, <class '__main__.D'>, <class '__main__.B'>, <type 'object'> )
>>> cl = CL()
init.CL
init.M
init.D
init.B

So what if we change the definition of M to this (and redefine the others unchanged):

class M(object):
  """ Mixin class """
  def __init__(self, *args, **kwds):
    print 'init.M'
    #super(M, self).__init__(*args, **kwds)
>>> print CL.__mro__
( <class '__main__.CL'>, <class '__main__.M'>, <class '__main__.D'>, <class '__main__.B'>, <type 'object'> )
>>> cl = CL()
init.CL
init.M

WOAH!!!! The MRO is the same, but the cooperative calling fails! The init methods of D and B are never called! Yikes. GvR talks about the cooperative calling pattern here, but he doesn't really get into the pitfalls of it. So I can't say exactly why, from an internals point of view the cooperative calling fails, except that I think it essentially falls back to the old-style class "first found" approach in the absence of the call to object.__init__.

So one more thing then, is invoking object.__init__ directly the same as invoking it via a call to super( cls, self ).__init__ where cls is a direct child of object? The answer is, to my surprise, no.

I redefined the M class from the Chandler Project example like this and reran the previous test.

class M(object):
  def __init__(self, *args, **kwds):
    print 'init.M'
    object.__init__(self, *args, **kwds)

The result was the same as if the super call was commented out!

>>> print CL.__mro__
( <class '__main__.CL'>, <class '__main__.M'>, <class '__main__.D'>, <class '__main__.B'>, <type 'object'> )
>>> cl = CL()
init.CL
init.M

Bottom line: be aware of the differences between "new style" and "old style" classes. Since "old style" classes are going away in Python , it's probably a good idea to get into the habit of making consistent and correct use of "new style" classes to avoid unpleasant surprises in complex inheritance situations.

-Ross


TrackBack

TrackBack URL for this entry:
http://blog.ncircle.com/cgi-bin/mt-tb.cgi/308


Post a comment

(If you haven't left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won't appear on the entry. Thanks for waiting.)

Verification (needed to reduce spam):



About

This page contains a single entry from the blog posted on November 11, 2008 9:06 AM.

The previous post in this blog was What does VERT do? .

The next post in this blog is A Great Way To Start a Monday.

Many more can be found on the main index page or by looking through the archives.



Bio

Blog: VERT
Author: nCircle VERT

nCircle VERT is the research team behind nCircle, continuously publishing updates for nCircle IP360 and nCircle's family of products. VERT conducts deep research across a broad class of network security intelligence, creating unique, agentless detection for: vunerabilities, host configurations, applications, services, user accounts, operating systems, and other network security conditions. Members of the group use this blog to share their opinions on the security industry, emerging threats, technology trends, and the world at large.


   




Categories