How to promulgate a list of arguments for a python function?

102 views Asked by At

I am trying to find an elegant pythonic way to solve the following problem. I have a class, VideoAnalysisWidget, that creates a whole bunch of child widgets such as VideoFileChoiceWidget, RegionWidget, etc. I want VideoAnalysisWidget.__init__ to pass any appropriate keyword arguments to VideoFileChoiceWidget.__init__, RegionWidget.__init__, ... To accomplish this, I would like to use code like the following:

import inspect

def filterArgs(func, **kwargs):
    spec = inspect.getargspec(func)
    ks = set(kwargs.keys())
    ks.discard('self')
    kinc = ks & set(spec.args)
    kexc = ks - set(spec.args)
    inc = {k: kwargs[k] for k in kinc}
    exc = {k: kwargs[k] for k in kexc}
    return(inc, exc)

Then, elsewhere:

def __init__(self, **kwargs):
    (fcwArgs, otherArgs) = filterArgs(VideoFileChoiceWidget.__init__, **kwargs)
    fcw = VideoFileChoiceWidget(**fcwArgs)
    ...

Doing things this way, my code for VideoAnalysisWidget doesn't have to keep track of the argument list of VideoFileChoiceWidget. If I later revise VideoFileChoiceWidget to take a new argument, I don't need to make changes in the distant VideoAnalysisWidget code.

Now here's the problem. This works fine if VideoFileChoiceWidget has only explicitly defined keyword parameters. (BTW, I'm fine with not using any positional parameters other than self for any of these functions.) But what if VideoFileChoiceWidget also has a **kwargs argument? It does, in fact, because it's subclassed from ContainerWidget, so I want to pass any extra extra arguments on to that. (And ContainerWidget takes ONLY **kwargs.) Fundamentally, the problem is that this solution of mine can't be nested.

A possible solution would be to attach a list of additional arguments to VideoFileChoiceWidget.__init__, e.g.:

VideoFileChoiceWidget.__init__._args = ['description', 'visible', 'children']

...then modify filterArgs to use this if available. But is there a better, more pythonic way to do it?

1

There are 1 answers

0
Leon Avery On BEST ANSWER

I'm going ahead with a variant on the approach I outlined in my original post, in which I attach an attribute to __init__. However, instead of making it a list, I am taking the more flexible approach of using a function. I define an unattached function argNames that takes one or more classes or functions as arguments and returns a set of argument names, using inspect and, recursively, calls to the argNames attribute of the functions. I can then attach this to VideoFileChoiceWidget.__init__ (for instance), using:

class VideoFileChoiceWidget(ContainerWidget):
    ...<stuff>...
    def __init__(arg1, arg2, ..., **kwargs):
        ...<more stuff>...

    __init__.argNames = lambda: (argNames(ContainerWidget) | 
                                 {'description', 'visible', 'children'})

Anyone who wants to pass arguments on to VideoFileChoiceWidget uses filterArgs as outlined in the question. I have implemented this and it seems to work.

Below, for reference, are my current definitions of argNames and filterArgs.

def argNames(*args):
    """
    Get the names of the argument for a function

    Returns:
    --------
    A set whose members are the argument names

    Parameters:
    -----------
    func1, func2, ... -- the functions to be analyzed. The arguments
        are discovered by inspecting func. In addition, if
        func.argNames is defined, it is called (without arguments) to
        get a list of addition names. Any number of functions may be
        listed. If func is a class, func.__init__ is used
        instead. 'self' is always removed from the argument list.
    """
    names = set({})
    for a in args:
        try:
            func = a
            spec = inspect.getargspec(func)
        except TypeError:
            func = a.__init__
            spec = inspect.getargspec(func)
        names |= set(spec.args)
        try:
            names |= set(func.argNames())
        except AttributeError:
            pass
    names.discard('self')
    return(names)

def filterArgs(func, **kwargs):
    """
    Select keyword arguments for a function
    """
    args = argNames(func)
    ks = set(kwargs.keys())
    ks.discard('self')
    kinc = ks & args
    kexc = ks - args
    inc = {k: kwargs[k] for k in kinc}
    exc = {k: kwargs[k] for k in kexc}
    return(inc, exc)