简体   繁体   中英

How can I generically pass params to super().__init__()?

Typically a subclass __init__ will just pass to super().__init__ all the params that it needs, then also do something else. Is there a neat way to do this, without copying the parameters in my code? In the example below, a,b,c,d,e appears three times. I'd like some "magic" to handle this. Something with args and kwargs ?.

class A:
  def __init__(self,a,b,c,d,e):
    # ... use params here ...
    pass

class B(A):
  def __init__(self,a,b,c,d,e,f,g,h):
    super().__init__(a,b,c,d,e)
    # do something with params f,g,h
    pass

This is most easily achieved with the dataclasses module new to python 3.7. It will only really help for relatively simple classes. As are pointed out in the comments dataclasses are not without their limitations. If you're just after a simple record-style class then it's a good way to reduce boilerplate code. For anything more complex, explicit is better than implicit. That is, writing out your own __init__ and doing what you need. For instance, altering arguments before passing them on to super, or adding new arguments that do not have defaults (when the super class does have default arguments).

from dataclasses import dataclass, field, InitVar, fields

@dataclass
class X:
    a: int
    # the dataclass decorator creates the following __init__ method
    # def __init__(self, a):
    #     self.a = a

@dataclass
class Y(X):
    b: int=2
    c: int=field(init=False) # c is a computed value and not passed to __init__
    op: InitVar[str]="add" # op is present in __init__, but not stored 
    # (it is still a class variable though, with value "add")

    # dataclass decorator creates an __init__method taking a, b, and op. But
    # it only stores b and passes a to super().__init__

    def __post_init__(self, op):
        # do some processing once fields have been initialised
        if op == "add":
            self.c = self.a + self.b
        elif op == "sub":
            self.c = self.a - self.b
        else:
            raise ValueError('invalid op')

x = X(1) # X instance with only a value
assert x.a == 1
y = Y(10, 20) # creating Y instance, passing both `a` and `b` (passing `c`
# would be an error)
assert y.a == 10 and y.b == 20 and y.c == 30
y1 = Y(100, op="sub") # creating Y instance with just field `a` and non-field `op`
assert y1.a == 100 and y1.b == 2 and y1.c == 98

Yes, you can use kwargs. You can use **kwargs to let your functions take an arbitrary number of keyword arguments ("kwargs" means "keyword arguments"). This is used here:

def __init__(self, *args, **kwargs)

When creating an instance of class, we need to unpack the kwargs, hence **kwargs.

b_instance = B(**test_kwargs)

The full code, based on your example is:

test_kwargs = {
    'a': 1,
    'b': 2,
    'c': 3,
    'd': 4,
    'e': 5,
    'f': 6,
    'g': 7,
}

class A():
    def __init__(self, *args, **kwargs):
        self.a = kwargs.get('a')
        self.b = kwargs.get('b')
        self.c = kwargs.get('c')
        self.d = kwargs.get('d')
        self.e = kwargs.get('e')

class B(A):
    def __init__(self, *args, **kwargs):
        # super(A, self).__init__(*args, **kwargs) resolves
        # to object base class which takes no parameter
        # You will get this error 
        # TypeError: object.__init__() takes no parameters

        super(B, self).__init__(*args, **kwargs)
        self.f = kwargs.get('f')
        self.g = kwargs.get('g')

    def print_all_instance_vars(self):
        # self.__dict__.items() =
        # dict_items(
        #   [('a', 1), ('b', 2), ('c', 3),
        #   ('d', 4), ('e', 5), ('f', 6),
        #   ('g', 7)])

        for key, value in self.__dict__.items():
            print("{}:{}".format(key, value))

b_instance = B(**test_kwargs)
b_instance.print_all_instance_var()

# a:1
# b:2
# c:3
# d:4
# e:5
# f:6
# g:7

In class B, you only have to assign its own instance variables as B inherits from A. Hence, B has all A's instance variables as its default values.

However, you might want to reconsider your design decisions if you have to write ugly code like this. Having too many instance variables can be confusing in the future.

From the Zen of Python:

Beautiful is better than ugly.

Explicit is better than implicit.

Simple is better than complex.

Readability counts.

Hope this helps!

Here is a solution. It requires modifying the baseclass slightly to give flexibility to the subclasses.

It also gives a subclass with an "ugly" interface (args a,b,c,d,e,f,g,h are required but not in the signature). But subclasses are generally more internal than baseclasses, so that ugliness could be OK.

class A:
  def __init__(self,a,b,c,d,e, **extra_args):
    # ... use params here ... extra_args are not used at all here.
    pass

class B(A):
  def __init__(self,*args, **kwargs):
    super().__init__(*args, **kwargs)
    f= kwargs["f"] 
    g= kwargs["g"]
    h= kwargs["h"]
    # do something with params f,g,h
    pass

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM