How can a function take **kwargs as its arguments and distribute them among different classes?

340 views Asked by At

Suppose I have two classes...

class A:

    def __init__(self, *args, arg1="default1", arg2="default2"):
        # Initialise class A


class B:

    def __init__(self, arg3="default3", arg4="default4"):
        # Initialise class B

Each class has its own keyword arguments, and one has positional arguments.

Now suppose there is a function which creates an instance of each of these classes, using its own arguments to do so:

def make_objs(*args, arg1="default1", arg2="default2", arg3="default3", arg4="default4"):
    objA = ClassA(*args, arg1=arg1, arg2=arg2)
    objB = ClassB(arg3=arg3, arg4=arg4)

Here I manually allocate the keyword arguments that the function received to the correct class. This is a bit tedious though - I have to duplicate the keywords in the function definition, and changing the classes will mean changing the function's arguments.

Ideally, I would do something like this:

def make_objs(*args, **kwargs):
    objA = ClassA(*args, **kwargs)
    objB = ClassB(**kwargs)

Where each class would take all the keyword arguments and extract only those which are relevant to it. That's not what the above code would actually do of course, it will throw an Exception because ClassA is not expecting an argument called arg3.

Is there anyway to do this? Some way of making the function take **kwargs as an argument and determine which arguments can go to which class?

3

There are 3 answers

2
martineau On

To avoid all the repetitious typing you could create a utility function which uses the inspect module to examine the calling sequence of the functions / methods involved.

Here's a runnable example of applying it to your code. The utility function is the one named get_kwarg_names:

from inspect import signature, Parameter

class ClassA:
    def __init__(self, *args, arg1="default1", arg2="default2"):
        print('ClassA.__init__(): '
              '*args={}, arg1={!r}, arg2={!r}'.format(args, arg1, arg2))

class ClassB:
    def __init__(self, arg3="default3", arg4="default4"):
        print('ClassB.__init__(): arg3={!r}, arg4={!r}'.format(arg3, arg4))

def get_kwarg_names(function):
    """ Return a list of keyword argument names function accepts. """
    sig = signature(function)
    keywords = []
    for param in sig.parameters.values():
        if(param.kind == Parameter.KEYWORD_ONLY or
            (param.kind == Parameter.POSITIONAL_OR_KEYWORD and
                param.default != Parameter.empty)):
            keywords.append(param.name)
    return keywords

# Sample usage of utility function above.
def make_objs(*args, arg1="default1", arg2="default2",
                     arg3="default3", arg4="default4"):

    local_namespace = locals()
    classA_kwargs = {keyword: local_namespace[keyword]
                        for keyword in get_kwarg_names(ClassA.__init__)}
    objA = ClassA(*args, **classA_kwargs)

    classB_kwargs = {keyword: local_namespace[keyword]
                        for keyword in get_kwarg_names(ClassB.__init__)}
    objB = ClassB(**classB_kwargs)

make_objs(1, 2, arg1="val1", arg2="val2", arg4="val4")

Output:

ClassA.__init__(): *args=(1, 2), arg1='val1', arg2='val2'
ClassB.__init__(): arg3='default3', arg4='val4'
0
wwii On
class A:
    def __init__(self, *args, a1 = 'd1', a2 = 'd2'):
        self.a1 = a1
        self.a2 = a2
class B:
    def __init__(self, a3 = 'd3', a4 = 'd4'):
        self.a3 = a3
        self.a4 = a4

Use inpect.signature to get the call signatures then filter kwargs before creating the objects.

from inspect import signature
from inspect import Parameter

sig_a = signature(A)
sig_b = signature(B)

def f(*args, **kwargs):
    d1 = {}
    d2 = {}
    for k, v in kwargs.items():
        try:
            if sig_a.parameters[k].kind in (Parameter.KEYWORD_ONLY,
                                            Parameter.POSITIONAL_OR_KEYWORD,
                                            Parameter.VAR_KEYWORD):
                 d1[k] = v
        except KeyError:
            pass
        try:
            if sig_b.parameters[k].kind in (Parameter.KEYWORD_ONLY,
                                            Parameter.POSITIONAL_OR_KEYWORD,
                                            Parameter.VAR_KEYWORD):
                d2[k] = v
        except KeyError:
            pass
    return (A(args, **d1), B(**d2))

d = {'a1':1, 'a2':2, 'a3':3, 'a4':4}   
x, y = f(2, **d)

>>> x.a1
1
>>> x.a2
2
>>> y.a3
3
>>> y.a4
4
>>> 

The checks to see if the Parameter is a keyword parameter may be overkill.

2
AudioBubble On

If you add *arg and **kwargs in __init__ method of your classes you can achieve the behavior you are expecting. Like in the exaple below:

class A(object):

    def __init__(self, a, b, *arg, **kwargs):
        self.a = a
        self.b = b

class B(object):

    def __init__(self, c, d, *arg, **kwargs):
        self.c = c
        self.d = d

def make_objs(**kwargs):
    objA = A(**kwargs)
    objB = B(**kwargs)

make_objs(a='apple', b='ball', c='charlie', d='delta')

But the caveat here is that if you print objA.c and objA.d it will return charlie and delta as passed in the make_objs parameters.