1

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 (e.g., 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 (i.e., 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().
ryparker
  • 13
  • 4
  • One way of doing it is to call the function like so: `func(**{'arg1': 1, 'arg2': 2})`, which is equivalent to `func(arg1=1, arg2=2)`. – Nick ODell Jul 07 '22 at 03:40
  • 1
    `In Python, the __repr__() method of an object returns a string that can be used to re-create the object by passing that string to eval()` This is only sort of true - there are lots of cases where this doesn't work. For example: `repr(print)`. – Nick ODell Jul 07 '22 at 03:42
  • Thanks for the clarification, @NickODell, I appreciate it and I've updated the question to say "the `__repr__()` method of an object returns a string that _sometimes_ can be used to re-create the object...". – ryparker Jul 07 '22 at 11:09

1 Answers1

3

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}
Grismar
  • 27,561
  • 4
  • 31
  • 54
  • This is excellent, thank you @Grismar for your helpful (and fast!) response. Much appreciated. `.join()` was just what I was looking for. – ryparker Jul 07 '22 at 04:47