Change default constructor argument value (inherited from parent class) in subclass

Question:

I have a Parent class with a default value for the attribute arg2. I want to create a subclass Child which has a different default value for the same attribute.
I need to use *args and **kwargs in Child.

I tried the following, but it is not working:

class Parent(object):
    def __init__(self, arg1='something', arg2='old default value'):
        self.arg1 = arg1
        self.arg2 = arg2

        print('arg1:', self.arg1)
        print('arg2:', self.arg2)

class Child(Parent):
    def __init__(self, *args, **kwargs):
        super(Child, self).__init__(*args, **kwargs)
        self.arg2 = kwargs.pop('arg2', 'new value')

This is not working. In fact, I get:

>>> c = Child()
arg1: something
arg2: default value # This is still the old value
>>> c.arg2
'new value' # Seems more or less ok

>>> c = Child('one', 'two')
arg1: one
arg2: two
>>> c.arg2
'new value' # This is wrong, it has overridden the specified argument 'two'
Asked By: Kurt Bourbaki

||

Answers:

You need to set the default in kwargs before passing it on to super(); this is tricky as you need to ensure that the same value is not already in args too:

class Child(Parent):
    def __init__(self, *args, **kwargs):
        if len(args) < 2 and 'arg2' not in kwargs:
            kwargs['arg2'] = 'new value'
        super(Child, self).__init__(*args, **kwargs)

This relies on knowing how many arguments are there to fill however. You’d have to use introspection of super().__init__ for this to work in the general case:

from inspect import getargspec

class Child(Parent):
    def __init__(self, *args, **kwargs):
        super_init = super().__init__
        argspec = getargspec(super_init)
        arg2_index = argspec.args.index('arg2') - 1  # account for self
        if len(args) < arg2_index and 'arg2' not in kwargs:
            kwargs['arg2'] = 'new value'
        super(Child, self).__init__(*args, **kwargs)

You’d be much better off specifying all defaults instead:

class Child(Parent):
    def __init__(self, arg1='something', arg2='new value'):
        super(Child, self).__init__(arg1=arg1, arg2=arg2)
Answered By: Martijn Pieters

You’ve actually changed the signature of the class. Basically, with:

def foo(a=1, b=2):
   ...

you can call by position, or by keyword:

foo(2, 3)
foo(a=2, b=3)

With:

def bar(**kwargs):
   ...

you can’t call with positional arguments any more:

bar(2, 3)  # TypeError!

Your actual code has additional complications because you have *args in there which eat up all of your positional arguments.


The most robust advice I can give you is to preserve the signature when you override the method:

class Child(Parent):
    def __init__(self, arg1='something', arg2='new value'):
        super(Child, self).__init__(arg1=arg1, arg2=arg2)

This (unfortunately) isn’t a DRY (Don’t Repeat Yourself) as you’d probably like — You have to specify 'something' twice. You could turn it into a global constant, or change the signature of Parent.__init__.

Alternatively, you could do a bunch of introspection to work with the signature of the parent class to make sure that you’re passing the correct arguments in the right ways — but I doubt very much that it’s worth it.

Answered By: mgilson

I wasn’t satisfied with either solution and came up with the one below. It introduces the defaults as class attributes which are loaded if the default is None:

#!/usr/bin/env python3

class Parent:
    _arg1_default = 'something'
    _arg2_default = 'old default value'

    def __init__(self, arg1=None, arg2=None):
        if arg1 is None:
            arg1 = self._arg1_default
        if arg2 is None:
            arg2 = self._arg2_default

        self.arg1 = arg1
        self.arg2 = arg2

        print('arg1:', self.arg1)
        print('arg2:', self.arg2)


class Child(Parent):
    _arg2_default = 'new value'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)


if __name__ == '__main__':
    print('Call Parent without arguments (use defaults)')
    parent = Parent()
    print('Call Child without arguments (use defaults)')
    child = Child()
    print('Call 2nd Child with custom arguments ("one", "two")')
    child2 = Child('one', 'two')
    print('Query arg2 of 2nd child')
    print(child2.arg2)

Yields:

Call Parent without arguments (use defaults)
arg1: something
arg2: old default value
Call Child without arguments (use defaults)
arg1: something
arg2: new value
Call 2nd Child with custom arguments ("one", "two")
arg1: one
arg2: two
Query arg2 of 2nd child
two   
Answered By: s_g