简体   繁体   中英

What is the opposite of unpacking **kwargs in Python?

I've been learning about Python decorators and to practice using them, I'm writing a logging decorator that records when a function is called, the *args and **kwargs passed to the function, and a __repr__ of the function so it can be re-evaluated at a later time.

In Python, the __repr__() method of an object returns a string that sometimes can be used to re-create the object by passing that string to eval() . I'm trying to implement a form of __repr__ for functions.

Since I'm using a decorator that will wrap any function, I won't know beforehand what the function's parameters are, so I need to use *args and **kwargs to construct the __repr__ , which is a string that looks like:

"function_name(positional_arg1, positional_arg2, keyword_arg1='some_value', keyword_arg2='other_value')"

To create that string representation, I need to reconstruct an argument list from **kwargs ; that is, convert a dictionary in the form of {'kwarg1': val1, 'kwarg2': val2} to an argument list like: kwarg1=val1, kwarg2=val2 .

(Note: unpacking *args to an argument list isn't a problem, since the tuple form of args is already an acceptable format for passing as positional arguments to a function after removing the parentheses from the tuple. That is, the args tuple: ('arg1', 'arg2') simply becomes 'arg1', 'arg2' . Thus, this question focuses on converting the kwargs dictionary back to an argument list.)

Below is what I have created so far. It works but isn't very elegant. Is there a simpler way to perform the opposite of unpacking kwargs ?

EDIT: I removed supplementary code (eg, setting up the decorator) to focus on the question at hand: the reverse operation of unpacking.

def print_args(*args, **kwargs):
    # Generate the kwargs string representation
    kwargs_repr = ''
    # If no kwargs are passed, kwargs holds an empty dictionary (i.e., dict())
    if kwargs != dict():
        num_kwargs = len(kwargs)
        # Convert to format required for an argument list (i.e., key1=val1 rather than {key1: val1})
        for n, (kw, val) in enumerate(kwargs.items()):
            kwargs_repr += str(kw) + '='
            # If the value is a string, it needs extra quotes so it stays a string after being passed to eval().
            if type(val) == str:
                kwargs_repr += '"' + val + '"'
            else:
                kwargs_repr += str(val)
            # Add commas to separate arguments, up until the last argument
            if n < num_kwargs - 1:
                kwargs_repr += ', '
    repr = (
        "print_args("
        # str(args)[1:-1] removes the parentheses around the tuple
        f"{str(args)[1:-1] if args != tuple() else ''}"
        f"{', ' if args != tuple() and kwargs != dict() else ''}"
        f"{kwargs_repr})"
    )
    print(repr)
    return repr


returned_repr = print_args('pos_arg1', 'pos_arg2', start=0, stop=10, step=1) 

Output:

print_args('pos_arg1', 'pos_arg2', start=0, stop=10, step=1)

(Attribution: some inspiration for my technique came from this Stack Overflow answer: https://stackoverflow.com/a/10717810/17005348 ).

Notes on what I've tried so far

  • I know I can access the args and kwargs separately, like this:
# If I know the name of the function 
# and have stored the args and kwargs passed to the function:
any_func(*args, **kwargs)

# Or, more generally:
kwargs = str(kwargs)[1:-1].replace(': ', '=').replace("'", '')
eval(
    # func_name holds the __name__ attribute of the function
    func_name 
    + '(' 
    + str(args)[1:-1] 
    + ',' 
    + str(kwargs) 
    + ')'
)

... but I'd like to use the repr form if possible, to mimic the syntax used for creating an object (ie, eval(repr(obj)) ) and to avoid the messy string concatenations used in the generic version.

  • List/tuple comprehensions: my attempts haven't worked because each key-value pair becomes a string, rather than the entire arguments list as a whole, which means that eval() doesn't recognize it as a list of keyword arguments. For example:
print(tuple(str(k) + '=' + str(v) for k, v in kwargs.items()))

outputs ('key1=val1', 'key2=val2') instead of ('key1=val1, key2=val2') .

  • Using the repr() function, like: repr(func(*args, **kwargs)) . This hasn't worked because the func() is evaluated first, so repr() just returns a string representation of the value returned by func() .

There's some serious caveats here, as not everything passed to any type of keyword argument will necessarily have a good representation and the representation may not work as you expect (reconstructing a copy of the original when evaluated).

Having said that, something like this:

def print_args(*args, **kwargs):
    print(', '.join(map(repr, args)),
          ', '.join(f'{k}={repr(v)}' for k, v in kwargs.items()))


print_args(1, 'test', [1, 2, 3], a='word', b={'key': 1, 'another': 2})

Output:

1, 'test', [1, 2, 3] a='word', b={'key': 1, 'another': 2}

Note: I didn't take the trouble of printing a comma between the two sections, I assume it's obvious what's going on there. But in case it annoys you:

def print_args(*args, **kwargs):
    print(', '.join(list(map(repr, args)) + 
                    [f'{k}={repr(v)}' for k, v in kwargs.items()]))

Output:

1, 'test', [1, 2, 3], a='word', b={'key': 1, 'another': 2}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM