Michael
Michael

Reputation: 677

Keyword for direct passthrough of **kwargs to super().__init__

Can there be a "magical keyword" (which obviously only works if no **kwargs is specified) so that the __init__(*args, ***pass_through_kwargs) so that all unexpected kwargs are directly passed through to the super().__init__? (in the background and without the users knowledge) This would make the readability much easier and it can be safely ignored by autocomplete and automated documentation.

In the following diagram Mouse is ever only instantiated with a name. Just to make the super-calls down the line work I have to add **kwargs (to specify the number_of_holes the cheese has) and hand them over by hand. Can't I just tell Mouse and Animal "Whatever you don't know: just pass it directly down to super()"? Class diagram with red path of <code>super()</code> calls

Of course you one might want to think about collisions, but I think this might greatly improve the readability of code

Upvotes: 1

Views: 622

Answers (1)

jbndlr
jbndlr

Reputation: 5210

Answer to Updated Question

This, indeed, is possible. Before providing some code for demonstration, let me just give a few notes:

  • This kind of automagic attribute access is really bad style. You cannot document it, it will let people misuse/ignore encapsulation concepts.

  • It is not self-explanatory at all; you cannot use duck typing, because suddenly every object can have arbitrary attributes.

  • What you really want to do is creating a consistent and understandable design among objects that have a clear focus and bring the corresponding functionality.

However, if you still want to test it, you can just go with **kwargs and pass it up to the super constructor; it always contains all keyword arguments that are not explicitly set in the method signature:

def testme(color='red', **kwargs):
    print(kwargs)

testme(width=3)

# See that 'color' is not in kwargs dict if running this code:
>>> {'width': 3}

This is the case, because explicitly set keyword arguments are assigned to the variable name given in the function signature; the rest is captured in kwargs.

This behavior allows you to actually filter those keyword arguments in each __init__ that are expected, and pass the rest upwards to the super class' __init__:

class Animal(object):
    def __init__(self, num_legs, **kwargs):
        self.num_legs = num_legs

        for key, value in kwargs.iteritems():
            if not hasattr(self, key):
                print(
                    'Generic attribute assignment: {:s} = {:s}'
                    .format(key, str(value)))
                setattr(self, key, value)

    def __str__(self):
        res = '<Animal ({:s}):\n'.format(self.__class__.__name__)
        for key in dir(self):
            if key.startswith('__'):
                continue
            res += '  {:s} = {:s}\n'.format(key, str(getattr(self, key)))
        res += '>'
        return res


class Mouse(Animal):
    def __init__(self, color='gray', **kwargs):
        super(Mouse, self).__init__(4, **kwargs)
        self.color = color


class MickeyMouse(Mouse):
    def __init__(self, **kwargs):
        super(MickeyMouse, self).__init__(color='black', **kwargs)


if __name__ == '__main__':
    mm = MickeyMouse(dog='Pluto')
    print(mm)

This will yield:

Generic attribute assignment: dog = Pluto
<Animal (MickeyMouse):
  color = black
  dog = Pluto
  num_legs = 4
>

The rationale is, that the keyword 'dog' is never expected in any of the __init__ methods and thus always remains in the kwargs, until the last super class' __init__ runs through the dict and sets the corresponding attributes. That's where the assignment message is printed. Afterwards, the specialized __init__ methods set their values (here: 'color') and you're done.

Can you please explain your figure? Do arrows represent dependencies, ownerships, inheritance, ...?

Previous/Outdated Answer

If your class MickeyMouse needs to call the init of both Cheese and Mouse, you need to be sure about their dependencies first.

Constructing a concrete object from a class definition yields a stateful representation of that very class. So if you create a MickeyMouse object, it may be required that it calls its super class' constructor, for example that one of class Mouse -- given MickeyMouse inherits from Mouse. If you now want the initialization of your MickeyMouse also to call the Cheese init, it would create a new object of that kind, essentially having two objects at hand: A mouse (i.e. MickeyMouse), and a cheese where nobody knows where it came from.

But maybe I still didn't get the application case right. Anyhow, concerning your implementation approach:

No Method Overloading

As you noticed, the __init__ method of a class is a single instance (magic) method you cannot overload. And since it is not possible by design, you should not try to overload, or even create method-swapping implementations that try to achieve overloading of class methods.

In your example, you state that you want to be able to create instances of a single class using different args and kwargs combinations and to magically resolve to the correct handling.

Possible Ways Out

(1) Use kwargs only:

class Cheese(object):
    def __init__(self, num_holes=0, weight=None, name=None):
        # Add logic depending on kwargs that are not None
        pass

Use it like so:

cheddar = Cheese()
gouda = Cheese(num_holes=3)
owned_cheddar = Cheese(name='MickeyMouse')

(2) Use static constructor methods.

class Cheese(object):
    def __init__(self, num_holes, weight, name):
        # Default construct using all args
        pass

    @staticmethod
    with_owner(name):
        return Cheese(0, None, name)

Use it like so:

cheddar = Cheese(0, None, None)
owned_cheddar = Cheese.with_owner('MickeyMouse')

Note that using static methods is kind of inflexible for your application; it is better used when constructing objects from different data structures, like .from_dict(...), .from_csv(...), etc.

Upvotes: 4

Related Questions