[Python-Dev] Breaking calls to object.__init__/__new__

Terry Jones terry at jon.es
Thu Mar 22 16:25:56 CET 2007


Following up to myself, with some examples.

I probably haven't done this as cleanly as is possible, but below are a
bunch of classes and subclasses that cleanly deal with passing around
arguments, even when those args conflict in name, etc., as outlined in my
previous mail.

Here's the general class __init__ pattern:

    class myclass(A, B, C, ...):
        def __init__(self, **kwargs):
            initargs = kwargs.setdefault('__initargs__', {})  # 1
            Aargs = initargs.setdefault(A, {})                # 2
            Aargs['limit'] = 10                               # 3
            Bargs = initargs.setdefault(B, {})                # 4
            Bargs['limit'] = 'wednesday'                      # 5
            super(myclass, self).__init__(**kwargs)           # 6
            myargs = initargs.get(myclass, {})                # 7
            limit = myargs.get('limit', 7)                    # 8
            if 'huh?' in myargs: raise Exception              # 9

In words, the steps are:

    1. Pull the __initargs__ key out of **kwargs, or create it. This gives you
       the top-level dictionary containing your own arguments, if any, and the
       args for your superclass(es), if any.

    2. Get the arguments for superclass A, and
    3. Add an argument for A.__init__
    4 & 5. Do the same for superclass B. We don't alter args for Superclass C.

    6. Call super, passing **kwargs along (this contains the original
       __initargs__ that was sent to us, or the one we made in step 1 if
       there was no prior __initargs__.

    7. Our arguments, if any, are in initargs too. Get them.
    8. Pull out one of our args, or set a default.
    9. Check we didn't get any unexpected args, etc.

Some comments:

- This isn't too ugly, I don't think, but it does require mentioning your
  class and superclasses by name. There are many advantages (see last mail)
  if you're willing to do this.

- You can combine positional args and explicit keyword args in a class if
  you choose, and you can then disambiguate or complain if you like (class
  sub4 in the example code below does this). You can also take those
  explicit args and stuff them into the __initargs__ for your superclasses,
  as/if needed. Explicit args might be a slight pain to deal with, but you
  need them as you don't want to destroy the simple class instantiation
  mechanism of python that passes args normally to __init__.

- It does require that classes cooperate to a small extent. E.g., you don't
  probably don't want the __init__ class in one of your super classes to go
  fiddling with initargs[myclass] before myclass even gets to see its own
  args (though this particular nit could be solved by having myclass copy /
  remove its args before calling super).

- A class could also optionally delete it args once done. In that case it's
  cleaner to have line 7 use initargs.setdefault(myclass, {}) so a del
  initargs[myclass] always just works.

- If you don't want to mess with the args to your superclasses, don't. In
  that case lines 2-5 go away in the above (and 1 and 6 could be swapped).

- You can add *args to all your __init__ methods. This allows your class to
  sit in the middle of a class chain where a lower method wants to pass a
  positional argument on to a higher class (that wasn't written according
  to the above pattern). So the args just gets passed along. It's not as
  nice, but it doesn't break anything (by silently not passing on any
  positional args that may be present). Classes written using the above
  pattern can just ignore all positional args.

- If I were encouraging doing something like the above in python proper,
  I think I'd allow line 1 to read

            initargs = kwargs.setdefault(None, {})

  which adopts the convention that the __init__ keywords are passed in the
  None slot of kwargs. Currently you can't do this, as python complains
  that all keyword args must be strings. While this goes against explicit
  is better that implicit, using __initargs__ as I have done is polluting
  the keyword space and would probably one day cause someone a problem.

There's some example code below.

Terry


class err(Exception): pass

class sum(object):
    def __init__(self, **kwargs):
        print "-> in sum"
        initargs = kwargs.setdefault('__initargs__', {})
        super(sum, self).__init__(**kwargs)
        myargs = initargs.get(sum, {})
        limit = myargs.get('limit', 5)
        print "my limit is %d" % limit

class daylimitmaker(object):
    def __init__(self, **kwargs):
        print "-> in daylimitmaker"
        initargs = kwargs.setdefault('__initargs__', {})
        super(daylimitmaker, self).__init__(**kwargs)
        myargs = initargs.get(daylimitmaker, {})
        limit = myargs.get('limit', 'sunday (last day of the week)')
        print "my limit is '%s'" % limit

class lastday(object):
    def __init__(self, **kwargs):
        print "-> in lastday"
        initargs = kwargs.setdefault('__initargs__', {})
        super(lastday, self).__init__(**kwargs)
        myargs = initargs.get(lastday, {})
        limit = myargs.get('limit', 'yesterday')
        print "my limit is '%s'" % limit

class onelimitarg(object):
    def __init__(self, **kwargs):
        print "-> in onelimitarg"
        initargs = kwargs.setdefault('__initargs__', {})
        super(onelimitarg, self).__init__(**kwargs)
        myargs = initargs.get(onelimitarg, {})
        if list(myargs.keys()) != ('limit'):
            raise err, "hey, someone passed me unknown arguments: %s" % myargs
        limit = myargs.get('limit', 1)
        print "my limit is %d" % limit

class sub0(sum):
    '''Subclass sum, and pass it an explicit limit.'''
    def __init__(self, **kwargs):
        print "-> in sub0"
        initargs = kwargs.setdefault('__initargs__', {})
        sumargs = initargs.setdefault(sum, {})
        sumargs['limit'] = 99
        super(sub0, self).__init__(**kwargs)

class sub1(sum, lastday):
    '''Subclass sum and lastday, passing both an explicit limit.'''
    def __init__(self, **kwargs):
        print "-> in sub1"
        initargs = kwargs.setdefault('__initargs__', {})
        sumargs = initargs.setdefault(sum, {})
        sumargs['limit'] = 4
        lastdayargs = initargs.setdefault(lastday, {})
        lastdayargs['limit'] = 'friday'
        super(sub1, self).__init__(**kwargs)
        myargs = initargs.get(sub1, {})
        limit = myargs.get('limit', 23)
        # etc...
        
class sub2(lastday, sum):
    '''Subclass lastday and sum (opposite order from sub1), passing both an explicit limit.'''
    def __init__(self, **kwargs):
        print "-> in sub2"
        initargs = kwargs.setdefault('__initargs__', {})
        sumargs = initargs.setdefault(sum, {})
        sumargs['limit'] = 10
        lastdayargs = initargs.setdefault(lastday, {})
        lastdayargs['limit'] = 'wednesday'
        super(sub2, self).__init__(**kwargs)
        myargs = initargs.get(sub2, {})
        # etc...

class sub3(lastday, sum):
    '''Subclass lastday and sum, letting both use their default limit.'''
    def __init__(self, **kwargs):
        print "-> in sub3"
        initargs = kwargs.setdefault('__initargs__', {})
        super(sub3, self).__init__(**kwargs)
        myargs = initargs.get(sub3, {})
        # etc...

class sub4(lastday, sum):
    '''Subclass lastday and sum, let our __init__ take a limit arg and check for ambiguity.'''
    def __init__(self, limit=10, **kwargs):
        '''This is deliberately ugly, but it can be done.'''
        print "-> in sub4"
        initargs = kwargs.setdefault('__initargs__', {})
        sumargs = initargs.setdefault(sum, {})
        sumargs['limit'] = limit
        super(sub4, self).__init__(**kwargs)
        myargs = initargs.get(sub4, {})
        if 'limit' in myargs:
            raise err, "hey, someone passed me an __initargs__ limit argument, but i like to get an explicit limit arg.\n"
        
class sub5(sub3, sub4):
    def __init__(self, **kwargs):
        print "-> in sub5"
        initargs = kwargs.setdefault('__initargs__', {})
        super(sub5, self).__init__(**kwargs)
        myargs = initargs.get(sub5, {})
        # etc...
        
class sub6(sum, daylimitmaker):
    def __init__(self, **kwargs):
        print "-> in sub6"
        initargs = kwargs.setdefault('__initargs__', {})
        super(sub6, self).__init__(**kwargs)
        myargs = initargs.get(sub6, {})
        # etc...
        
class sub7(onelimitarg):
    def __init__(self, **kwargs):
        print "-> in sub7"
        initargs = kwargs.setdefault('__initargs__', {})
        onelimitargs = initargs.setdefault(onelimitarg, {})
        onelimitargs['limit'] = 10
        onelimitargs['dummy'] = 'dummy' # Pass an unknown arg.
        super(sub7, self).__init__(**kwargs)

if __name__ == '__main__':
    for klass in (sub0, sub1, sub2, sub3, sub4, sub5, sub6, sub7):
        print "Instantiating %s: mro = %s" % (klass.__name__, [k.__name__ for k in klass.__mro__])
        try:
            klass()
        except err, e:
            print e
        print
        
    print "Instantiating sub4(limit=22)"
    sub4(limit=22) # Call with explicit arg, the way sub4 likes it.
    print
    
    try:
        print "Instantiating sub4 with __initargs__ limit."
         # (The way sub4 doesn't like it.)
        sub4(__initargs__={sub4 : {'limit' : None}})
    except err, e:
        print e


More information about the Python-Dev mailing list