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
andkwargs
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 thefunc()
is evaluated first, sorepr()
just returns a string representation of the value returned byfunc()
.