Search code examples
pythonfunctionparametersfunctional-programmingsignature

Determine the arguments and keyword arguments of a function


How you determine the form of a valid call to a function?

For example, say we have a function info that accomplishes this; info might work like this (I'm open to suggestions on whatever might be a more complete and more consistent way to represent the information returned):

def foo():
    pass

info(foo)
# { 'args': (), 'kwargs': {} }

def bar(a):
    pass

info(bar)
# { 'args': ('a',), 'kwargs': {} }

def baz(a, b=42):
    pass

info(baz)
# { 'args': ('a',), 'kwargs': { 'b': 42 } }

def qux(a, *args, b=42, **kwargs):
    pass

info(qux)
# { 'args': ('a',), 'kwargs': { 'b': 42 }, 'optional': {'*args', '**kwargs'} }

The info function should work for any function. I am not sure how to write an example return for every pattern: For example, help(range.__init__) displays

# __init__(self, /, *args`, **kwargs)

and I am not sure what the / means.

The return from info needs to be something that be computed on (with reasonable effort) for the production of arbitrary, correct calls to info's argument, e.g., for randomized testing.


Solution

  • There is already a function for this purpose, inspect.getfullargspec which returns namedtuples:

    >>> import inspect
    >>> inspect.getfullargspec(foo)
    FullArgSpec(args=[], varargs=None, varkw=None, defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={})
    >>> inspect.getfullargspec(qux)
    FullArgSpec(args=['a'], varargs='args', varkw='kwargs', defaults=None, kwonlyargs=['b'], kwonlydefaults={'b': 42}, annotations={})
    >>> inspect.getfullargspec(bar)
    FullArgSpec(args=['a'], varargs=None, varkw=None, defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={})
    >>> inspect.getfullargspec(baz)
    FullArgSpec(args=['a', 'b'], varargs=None, varkw=None, defaults=(42,), kwonlyargs=[], kwonlydefaults=None, annotations={})
    

    However, if you want, you can build a function from this:

    def info(func):
        """returns function argument info"""
        specs = inspect.getfullargspec(func)
        dict_ = {}
        dict_['args'] = tuple(specs.args)
        dict_['kwargs'] = {} if specs.kwonlydefaults is None else specs.kwonlydefaults
        dict_['optional'] = set()
        dict_['defaults'] = {} if specs.defaults is None else specs.defaults
        if specs.varargs is not None:
            dict_['optional'].add(f"*{specs.varargs}")
        if specs.varkw is not None:
            dict_['optional'].add(f"*{specs.varkw}")
        if not dict_['optional']:
            dict_['optional'] = {}
        return dict_
    
    >>> info(foo)
    {'args': (), 'kwargs': {}, 'optional': {}, 'defaults': {}}
    
    >>> info(qux)
    {'args': ('a',), 'kwargs': {'b': 42}, 'optional': {'*args', '*kwargs'}, 'defaults': {}}
    
    >>> info(bar)
    {'args': ('a',), 'kwargs': {}, 'optional': {}, 'defaults': {}}
    
    >> info(baz)
    {'args': ('a', 'b'), 'kwargs': {}, 'optional': {}, 'defaults': (42,)}
    

    The 42 in baz is not a keyword argument, it is a default one. Because while calling it is not necessary to provide the keyword b.

    The * in the help(__init__) refers to keyword only parameters to follow, i.e. it tells the following arguments must be keyword-only arguments, and similarly any argument preceding / has to be positional argument, for more see PEP457 , PEP570, PEP3102.

    Many of these information can be obtained form the inherent code object of the function, which has following attributes:

    for attr in dir(qux.__code__):
        if not attr.startswith('_'):
            print(attr,':',getattr(qux.__code__, attr))
    
    co_argcount : 1
    co_cellvars : ()
    co_code : b'd\x00S\x00'
    co_consts : (None,)
    co_filename : <ipython-input-43-6608913c4d65>
    co_firstlineno : 1
    co_flags : 79
    co_freevars : ()
    co_kwonlyargcount : 1
    co_lnotab : b'\x00\x01'
    co_name : qux
    co_names : ()
    co_nlocals : 4
    co_stacksize : 1
    co_varnames : ('a', 'b', 'args', 'kwargs')
    

    However, these are not descriptive enough, nor easy to access and intended for internal use for python. Hence unless you absolutely need a custom function, inspect.getfullargspec is probably the best option.

    Output of fullargspec being a namedtuple you can access different fields easily:

    >>> argspecs = inspect.getfullargspec(qux)
    >>> argspecs.args
    ['a']
    >>> argspecs.kwonlydefaults
    {'b': 42}
    

    And if you want a dict you can call the _asdict method of the resulting namedtuple:

    >>> inspect.getfullargspec(qux)._asdict()  #gives OrderedDict
    OrderedDict([('args', ['a']),
                 ('varargs', 'args'),
                 ('varkw', 'kwargs'),
                 ('defaults', None),
                 ('kwonlyargs', ['b']),
                 ('kwonlydefaults', {'b': 42}),
                 ('annotations', {})])
    >>> dict(inspect.getfullargspec(qux)._asdict()) #call dict to get regular dict
    {'args': ['a'],
     'varargs': 'args',
     'varkw': 'kwargs',
     'defaults': None,
     'kwonlyargs': ['b'],
     'kwonlydefaults': {'b': 42},
     'annotations': {}}