0

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?

Sam Ireland
  • 512
  • 4
  • 20
  • How about for the `constructor` arguments in `class A` you add `*arg` and `**kwarg` after `arg1` and `arg2`? Same goes for `class B`. –  Jan 05 '17 at 02:29
  • Question: When are you going to unpack the `*args` in `classA`? Can you please provide the full definition of your `constructor` for `classA`? –  Jan 05 '17 at 03:02
  • The function is actually: https://github.com/samirelanduk/quickplots/blob/2.0/quickplots/quick.py#L4 – Sam Ireland Jan 05 '17 at 03:10
  • Class A is https://github.com/samirelanduk/quickplots/blob/2.0/quickplots/series.py#L133 – Sam Ireland Jan 05 '17 at 03:11
  • Class B is https://github.com/samirelanduk/quickplots/blob/2.0/quickplots/charts.py#L61 – Sam Ireland Jan 05 '17 at 03:12
  • You want to keep the class call signatures the same? ... with default values? – wwii Jan 05 '17 at 04:52
  • Take a look at [Separating **kwargs for different functions](http://stackoverflow.com/questions/23430248/separating-kwargs-for-different-functions?rq=1) and [Can you list the keyword arguments a Python function receives?](http://stackoverflow.com/questions/196960/can-you-list-the-keyword-arguments-a-python-function-receives) - You can probably fashion a solution based on one of the answers to those questions. – wwii Jan 05 '17 at 05:26

3 Answers3

1

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'
martineau
  • 119,623
  • 25
  • 170
  • 301
  • I like this better, I was tempted to make it a function that returned valid parameters but didn't get around to it. – wwii Jan 06 '17 at 15:08
  • Good. I tried to make something fairly generic so it could be easily modified if necessary and reused if desired. However, note it does require that the keyword arguments to all be unique. If my answer solves your problem, please consider accepting it. – martineau Jan 06 '17 at 16:29
0

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.

  • Sorry, I should have specified that I am already using *args for classA. classA's __init__ is `(self, *args, **kwargs)` and the function takes `(*args, **kwargs)`. – Sam Ireland Jan 05 '17 at 02:44
  • Can you update your question again with that information? Thanks. Also can you be more specific to what were you expecting to achieve. I may not be clear on your question. –  Jan 05 '17 at 02:47
0
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.

wwii
  • 23,232
  • 7
  • 37
  • 77