Reputation: 677
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()
"?
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
Reputation: 5210
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, ...?
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:
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.
(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