How to minimize redundancy in passing kwargs to multiple super classes? - python

Given the following classes A, B, and C:
class A:
def __init__(self, a, aa, aaa):
self.a = a
self.aa = aa
self.aaa = aaa
class B:
def __init__(self, b, bb, bbb):
self.b = b
self.bb = bb
self.bbb = bbb
class C(A, B):
def __init__(self, **kwargs):
super(C, self).__init__(**kwargs)
I want to avoid having to repeat all the superclasses parameters a, aa, aaa, b, bb, bbb, in C definition:
class C(A, B):
def __init__(self, a, aa, aaa, b, bb, bbb):
super(C, self).__init__(**kwargs)
and somehow pass A and B kwargs to be resolved in super().__init__ call but this is not possible using the way I described and will result in an error:
>>> c = C(a=1, aa=2, aaa=3, b=4, bb=5, bbb=6)
TypeError: A.__init__() got an unexpected keyword argument 'b'
The correct way of doing so is calling A.__init__(self, **a_kwargs) and B.__init__(self, **b_kwargs) but as I said this creates redundant parameters I'm trying to avoid. Is there a better way to achieve the same thing?

You can do this:
class A:
def __init__(self, a, aa, aaa, **kwargs):
self.a = a
self.aa = aa
self.aaa = aaa
super().__init__(**kwargs)
class B:
def __init__(self, b, bb, bbb, **kwargs):
self.b = b
self.bb = bb
self.bbb = bbb
super().__init__(**kwargs)
class C(A, B):
pass
c = C(a=1, aa=2, aaa=3, b=4, bb=5, bbb=6)
Since the C's MRO is [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>], A will consume it's parameters and pass the rest to B.
If you need to pass positional arguments too, You can change it to:
class A:
def __init__(self, a, aa, aaa, *args, **kwargs):
self.a = a
self.aa = aa
self.aaa = aaa
super().__init__(*args, **kwargs)
class B:
def __init__(self, b, bb, bbb, *args, **kwargs):
self.b = b
self.bb = bb
self.bbb = bbb
super().__init__(*args, **kwargs)
class C(A, B):
pass

You could use built-in signature(callable) function to obtain expected keyword arguments for each __init__ and pass only those that the function expects like so. Whether this is good design, that is a matter of another discussion.
from inspect import signature
class A:
def __init__(self, a, aa, aaa):
self.a = a
self.aa = aa
self.aaa = aaa
print(f"A obj: {a=}, {aa=}, {aaa=}")
class B:
def __init__(self, b, bb, bbb):
self.b = b
self.bb = bb
self.bbb = bbb
print(f"B obj: {b=}, {bb=}, {bbb=}")
class C(A, B):
def __init__(self, **kwargs):
A_params = {k:v for k, v in kwargs.items() if k in signature(A.__init__).parameters.keys()}
A.__init__(self, **A_params)
B_params = {k:v for k, v in kwargs.items() if k in signature(B.__init__).parameters.keys()}
B.__init__(self, **B_params)
c = C(a=1, aa=2, aaa=3, b=4, bb=5, bbb=6)
Output:
A obj: a=1, aa=2, aaa=3
B obj: b=4, bb=5, bbb=6

Related

Best way of solving diamond problem in Python with fields

Python solves the diamond problem well if there are no fields in the classes by linearizing the method resolution order. However, if the classes have fields then how do you call the super constructors? Consider:
class A:
def __init__(self, a):
self.a = a # Should only be initialized once.
class B(A):
def __init__(self, a, b):
super().__init__(a)
self.b = b
class C(A):
def __init__(self, a, c, b=None):
super().__init__(a)
self.c = c
class D(C, B):
def __init__(self, a, b, c):
super().??? # What do you put in here.
For my use case I do actually have a solution, because b can't be None in the application and therefore the following largely works:
class A:
def __init__(self, a):
self.a = a # Should only be initialized once.
class B(A):
def __init__(self, a, b):
assert b is not None # Special case of `b` can't be `None`.
super().__init__(a)
self.b = b
class C(A):
def __init__(self, a, c, b=None): # Special init with default sentinel `b`.
if b is None:
super().__init__(a) # Normally `C`'s super is `A`.
else:
super().__init__(a, b) # From `D` though, `C`'s super is `B`.
self.c = c
class D(C, B): # Note order, `C`'s init is super init.
def __init__(self, a, b, c):
super().__init__(a, c, b)
def main():
A('a')
B('b', 1)
C('c', 2)
D('d', 3, 4)
C('c2', 5, 6) # TypeError: __init__() takes 2 positional arguments but 3 were given
This largely works for the special case of b can't be None, however it still has a problem if C's __init__ is called directly (see last line of above). Also you have to modify C for the multiple inheritance and you have to inherit in the order C, B.
==== Edit ===
Another possibility is to manually initialize each field (this is somewhat similar to how Scala handles fields under the covers).
class A0:
def __init__(self, a): # Special separate init of `a`.
self._init_a(a)
def _init_a(self, a):
self.a = a
class B0(A0):
def __init__(self, a, b): # Special separate init of `b`.
self._init_a(a)
self._init_b(b)
def _init_b(self, b):
self.b = b
class C0(A0):
def __init__(self, a, c): # Special separate init of `c`.
self._init_a(a)
self._init_c(c)
def _init_c(self, c):
self.c = c
class D0(C0, B0):
def __init__(self, a, b, c): # Uses special separate inits of `a`, `b`, and `c`.
self._init_a(a)
self._init_b(b)
self._init_c(c)
The disadvantage of this approach is that it is very non-standard, to the extent that PyCharm gives a warning about not calling super init.
==== End edit ===
Is there a better way?
Thanks in advance for any help, Howard.

Second constructor when standard constructor has superfluous information

I have the following situation:
class Foo:
def __init__(self, O):
self.a = O.some_attr.calc_a()
self.b = O.some_other_attr.calc_b()
Note that O cannot be reconstructed from a and b. Now, I also want to be able to initialise Foo directly by passing a and b, but I only want to do this internally, the standard way should be by passing O.
I know I can do something like
class Foo:
def __init__(self, a, b):
self.a = a
self.b = b
#classmethod
def from_O(cls, O):
return cls(O.some_attr.cal_a(), O.some_other_attr.cal_b())
but this has the disadvantage that the standard call now becomes the more cumbersome Foo.from_O(O).
In other words, how can I achieve Foo(O) and Foo.from_a_b(a,b) when O is not reconstructable from a and b?
Can I have a classmethod that avoids calling __init__?
(Note: I am looking for a 'clean' way to do this. I know I can dissect the argument list or do something like
class _Foo:
def __init__(self, a, b):
self.a = a
self.b = b
class Foo(_Foo):
def __init__(self, O):
super().__init__(O.some_attr.cal_a(), O.some_other_attr.cal_b())
but this seems a rather awkward solution.)
You could make O, a, and b all optional arguments to the same __init__ method and make a distinction on whether O is given or not.
class Foo:
def __init__(self, O=None, a=None, b=None):
if O is not None:
self.a = O.some_attr.calc_a()
self.b = O.some_other_attr.calc_b()
# ignore a and b
else:
if a is None or b is None:
raise TypeError("If O is not given, a and b cannot be None")
self.a = a
self.b = b
Usage:
# from O
foo_from_O = Foo(O)
# from a, b
foo_from_a_b_1 = Foo(None, 'a', 'b')
foo_from_a_b_2 = Foo(a='a', b='b')

Understanding super in Python

Could you please explain to me how to write the deadly diamond in Python? I saw many examples of similar code without using constructor arguments, but once I start using arguments thigs start being messy...
class A:
def __init__(self, a):
self.a = a
class B(A):
def __init__(self, a, b):
self.b = b
super().__init__(a)
class C(A):
def __init__(self, a, c):
self.c = c
super().__init__(a)
class D(B, C):
def __init__(self, a, b, c, d):
self.d = d
# How do I pass a and b to B.__init__
# and a and c to C.__init__
# using super() ?
super().__init__(a, b, c) #???
d = D(1, 2, 3, 4)
The simplest way would probably be that every subclass accepts a dict of kwargs, and passes it to the upper level:
class A:
def __init__(self, a):
self.a = a
class B(A):
def __init__(self, b, **kwargs):
self.b = b
super().__init__(**kwargs)
class C(A):
def __init__(self, c, **kwargs):
self.c = c
super().__init__(**kwargs)
class D(B, C):
def __init__(self, d, **kwargs):
self.d = d
super().__init__(**kwargs)
d = D(a=1, b=2, c=3, d=4)
print(d.a, d.b, d.c, d.d)
# 1 2 3 4
At each call, __init__ gets the argument it requires, and passes the remaining ones to the parent class. The only drawback is that you have to pass the arguments as keywords.
Your subclasses initializers don't have compatible signatures, which is the key to proper cooperative super calls.
The simple solution here is to make those signatures compatible - canonically by adding support for varargs and arbitrary keyword args all along the chain, ie:
# NB: Python3 required, won't work in Py2
class A:
def __init__(self, a, *args, **kwargs):
self.a = a
class B(A):
def __init__(self, a, b, *args, **kwargs):
super().__init__(a, *args, **kwargs)
self.b = b
class C(A):
def __init__(self, a, c, *args, **kwargs):
super().__init__(a, *args, **kwargs)
self.c = c
class D(B, C):
def __init__(self, a, b, c, d, *args, **kwargs):
super().__init__(a, b=b, c=c, *args, **kwargs)
self.d = d
d = D(1, 2, 3, 4)
This will of course not solve each and every possible issue with multiple inheritance and cooperative super calls, but MI is tricky, and there are restrictions to what's possible anyway.

Passing arguments to superclass constructor without repeating them in childclass constructor

class P(object):
def __init__(self, a, b):
self.a = a
self.b = b
class C(P):
def __init__(self, c):
P.__init__()
self.c = c
obj = C(a, b, c) #want to instantiate a C with something like this
I want to define C class object without rewriting all the P class constructor argument in C's constructor, but the above code doesn't seem to work. What is the right approach to do this?
Clarification:
The idea is to avoid putting parent class's constructor arguments in child class's constructor. It's just repeating too much. All my parent and child classes have many arguments to take in for constructors, so repeating them again and again is not very productive and difficult to maintain. I'm trying to see if I can only define what's unique for the child class in its constructor, but still initialize inherited attributes.
In Python2, you write
class C(P):
def __init__(self, a, b, c):
super(C, self).__init__(a, b)
self.c = c
where the first argument to super is the child class and the second argument is the instance of the object which you want to have a reference to as an instance of its parent class.
In Python 3, super has superpowers and you can write
class C(P):
def __init__(self, a, b, c):
super().__init__(a, b)
self.c = c
Demo:
obj = C(1, 2, 3)
print(obj.a, obj.b, obj.c) # 1 2 3
Response to your comment:
You could achieve that effect with the *args or **kwargs syntax, for example:
class C(P):
def __init__(self, c, *args):
super(C, self).__init__(*args)
self.c = c
obj = C(3, 1, 2)
print(obj.a, obj.b, obj.c) # 1 2 3
or
class C(P):
def __init__(self, c, **kwargs):
super(C, self).__init__(**kwargs)
self.c = c
obj = C(3, a=1, b=2)
print(obj.a, obj.b, obj.c) # 1 2 3
obj = C(a=1, b=2, c=3)
print(obj.a, obj.b, obj.c) # 1 2 3
You can call parent class constructor by passing self and required arguments
class C(P):
def __init__(self, a,b,c):
P.__init__(self,a,b)
self.c = c

Super with arguments in case of multiple inheritance

I have a class that inherits from two other classes whose __init__ take both a parameter like this:
class A(object):
def __init__(self, a):
self.a = a
class B(object):
def __init__(self, b):
self.b = b
class C(A, B):
def __init__(self, a, b):
super(C, self).__init__(a, b)
c = ClassC(1, 2)
This gives a TypeError: __init__() takes exactly 2 arguments (3 given).
When setting b in B to a fixed value and passing only 1 parameter to super then trying to access b in C gives an AttributeError: 'ClassC' object has no attribute 'b':
class A(object):
def __init__(self, a):
self.a = a
class B(object):
def __init__(self, b):
self.b = 2
class C(A, B):
def __init__(self, a, b):
super(C, self).__init__(a)
print self.a
print self.b
c = ClassC(1, 2)
When calling the __init__ manually everything seems to be fine:
class A(object):
def __init__(self, a):
self.a = a
class B(object):
def __init__(self, b):
self.b = b
class C(A, B):
def __init__(self, a, b):
A.__init__(a)
B.__init__(b)
print self.a
print self.b
c = ClassC(1, 2)
So how can I get this inheritance straight and how can I manage the parameters for __init__ of inherited classes when using super? Is it even possible? How does super know which parameters are to pass to which class?

Categories