Python: From Args to Kwargs

Posted in Python

permalink

Overview

In this short blog post, we talk about how and when you can take a method signature that defines input positional arguments by name, like this:

def foo(arg1, arg2, arg3):
    pass

and write code that will return a dictionary containing a keyword arguments-like structure:

>>> foo('red', 'blue', 'green')
{
    'arg1': 'red',
    'arg2': 'blue',
    'arg3': 'green'
}

We will cover an example of writing a decorator that utilizes input arguments from both the decorator and from the function it wraps, and how to keep all of that information straight.

The Easy Way: locals()

We'll start with the easiest possible wyay to turn args into kwargs: locals(). The locals() function is one of the built-in methods provided by Python:

>>> print(help(locals))

Help on built-in function locals in module builtins:

locals()
    Return a dictionary containing the current scope's local variables.

    NOTE: Whether or not updates to this dictionary will affect name lookups in
    the local scope and vice-versa is *implementation dependent* and not
    covered by any backwards compatibility guarantees.

This is a straightforward way to get a dictionary of input argument names mapping to the values provided by the user:

>>> def foo(arg1, arg2, arg3):
...     print(locals())
...
>>> foo('asdf', 'qwerty', 'oioioioi')
{'arg3': 'oioioioi', 'arg2': 'qwerty', 'arg1': 'asdf'}

When locals() Won't Work: Getting a Method Signature Programmatically

Sometimes, locals() won't get you what you need - like when you're decorating a function, and you don't have the original method signature.

In that case, you can still use a function handle and get the original positional argument names from the function signature.

In Python, the signature of a method can be obtained using the inspect module's signature() method, which can be passed a function:

>>> import inspect
>>> def foo(arg1, arg2, arg3):
...      pass
...
>>> print(inspect.signature(foo))
(arg1, arg2, arg3)

The _parameters attribute of the signature will yield an ordered list of parameters in the method signature, which is equivalent to the variable names that are used in the method definition (arg1, arg2, and arg3 in the example foo() function above):

>>> print(list(inspect.signature(foo)._parameters))
['arg1', 'arg2', 'arg3']

Args to Kwargs: Parameter Extraction from Decorator

We can use this to get the original variable names from a function handle, even if we don't have its original method signature (i.e., if we're a decorator and are just passed the function).

Here is an example of a decorator that extracts positional arguments from the function it decorates (and prints them out!):

import inspect

def real_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):

        # This is where the interesting stuff starts!
        # We have a handle to a function that we're
        # decorating, but we don't have its original
        # method signature.
        # No sweat. Turn positional args into augmented
        # kwargs!
        func_kwargs = {}
        sig = inspect.signature(wrapper)
        for i, p in enumerate(list(sig._parameters)):
            try:
                func_kwargs[p] = args[i]
            except IndexError:
                # Unspecified positional argument
                # (using default value)
                pass

        print(f"wrapper extracted the following params: {func_kwargs}")
        func(*args, **kwargs)

    return wrapper

# don't forget to top it off
# by decorating a simple function
# and calling it if script is run
@real_decorator
def foo(arg1, arg2, arg3):
    print("hello world!")

if  __name__=="__main__":
    foo('asdf', 'qwerty', 'wioioioio')

And when run, the result is:

$ py five.py
wrapper extracted the following params: {'arg1': 'asdf', 'arg2': 'qwerty', 'arg3': 'wioioioio'}
hello world!

Tags:    python    programming    arguments    functions    methods    parameters