1

I am fighting with one of my recurring nightmares of not knowing what is the best way to manage arguments for __init__ in a small hierarchy of classes. I have read several Q&A and articles on how good is to use super() and I understand it, however many examples just handle the basic case of __init__ without arguments, that is seldom my case.

To make an example, I have a class root with properties tags and description, it looks like:

class Root(object):
    """An object that can be tagged."""
    def __init__(tags = None, description = None):
        self.tags, self.description = tags, description

Now I can subclass, but what about the arguments tags and description? I can accept them explicitely in child class:

class Derived(Root):
    def __init__(self,tags = None, description = None):
        super().__init__(tags = tags, description = description)

or I can pass them by *args, **kwargs

The first solution forces me to rewrite the routine descriptor (and docs and docstrings) for all derived classes if I decide e.g. to add or remove an argument to/from Root. The second works automatically, but it hides the argument names from the introspection; and needs to be manually filtered if any other routine needs to accept optional arguments from *args, **kwargs.

A third way might be to explicity reassign the properties in child classes, without calling super.__init__, but this looks duplicated code to me, so it's probably not good. What is the best way to handle this situation?

0

2 Answers 2

3

First: super was created specifically to support multiple inheritance. When using it, you should not assume that you can predict which classes self is an instance of solely by looking at the base class of your class.


You should use *kwargs, but not just to pass tags and description. Instead, it is used to accept arbitrary keyword arguments that some other class in the MRO might need, so you can pass them on as well. If done correctly, then kwargs will be empty by the time object.__init__ is invoked.

class Root(object):
    """An object that can be tagged."""
    def __init__(self, tags=None, description=None, **kwargs):
        # If object is next, kwargs should be empty
        # Otherwise, it has arguments that Root doesn't know or care
        # about, but some other class does.
        super().__init__(**kwargs)
        self.tags, self.description = tags, description


class Derived(Root):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # Other stuff here


d = Derived(tags="hello", description="world")

Note that if Derived.__init__ does nothing except call super().__init__, you don't need to define it at all.

All calls to __init__ should be done with keyword arguments to avoid any potential conflicts over who gets what positional arguments.


An example:

class SomethingElse:
    def __init__(self, color, **kwargs):
        super().__init__(**kwargs)
        self.color = color

class Foo(Derived, SomethingElse):
    def __init__(self, size, **kwargs):
        super().__init__(**kwargs)
        self.size = size


f = Foo(size="large", tags="loud,noisy", color="red", description="strange")

Foo.__init__ is called first. It recognizes size and leaves the rest of the arguments in kwargs. Next is Derived.__init__; it just passes everything on the next method, which is Root.__init__. It recognizes tags and description, and leaves color to pass to the next method, SomethingElse.__init__. That method recognizes color, and passes anything else on to the next, and in this case final, method, object.__init__. Because each class was careful to remove the argument it introduced, by this point kwargs is empty, so that nothing is passed on to object.__init__, as it expects.

Sign up to request clarification or add additional context in comments.

Comments

1

I recommend using *args followed by **kwargs to allow the user of the class to pass tags and description in a positional manner if they want. It might make your constructors more complicated, as the logic to figure out which contains the each argument to the super class constructor but it'll make for a better experience for in future use both by you and by other developers.

class Root(object):
    """An object that can be tagged."""
    def __init__(self, tags = None, description = None):
        self.tags, self.description = tags, description

class Derived(Root):
    def __init__ (self, *args, *kwargs):
        if len(args) >= 2:
            # Both args to Root.__init__ were passed as positional arguments
            super().__init__(*args)

        elif len(args) == 1:
            # One of the arguments was passed as a named argument.
            super().__init__(*args, kwargs["description"])

        else:
            # Both arguments passed as named args.
            super().__init__(**kwargs)

        ####### Other stuff here #######

Sure, it's more work, but you'll be happier down the road when the constructor you've written doesn't force you to pass all your arguments as named arguments.

3 Comments

Using *args can run into problems once you start using multiple inheritance, since you won't necessarily know which method will try to consume which positional arguments.
That’s true. One could adhere to a convention like positional arguments always go to the super class, but then what’s one supposed to do if they don’t know that’s the convention when they look at some code for the first time, or it’s old code... This really is a challenging question.
The whole point of super is to support the case where there is no notion of "the" super class. If Python didn't support multiple inheritance, it wouldn't have bothered introducing super at all; Root.__init__(self, *args, **kwargs) would have sufficed, and you could more easily required that subclasses introduce new arguments following those of its single parent).

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.