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 sometimes returns a string that 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
?
# Create a class to store the state, so the logs can be recorded.
# The alternative is to use a global-scope dictionary and write
# to it, which is a much simpler implementation but not the
# best way to approach this problem.
class LoggerUtil():
def __init__(self):
self.record = {}
self.count = 0
# This is a triple-nested function because, when used as a decorator,
# it will need to be called (e.g., "@LoggerUtil.log()", with parentheses at the end).
# That is because the outermost level needs to retain a reference to 'self'.
def log(self):
def log_outer(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
# =======================================
# MY IMPLEMENTATION OF REVERSE-UNPACKING
# =======================================
# 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)
# Loop through kwargs dictionary and convert to the
# 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 = ', '
# =======================================
# END OF REVERSE-UNPACKING
# =======================================
# Save log to the record
self.record[self.count] = {
'function': func.__name__,
'args': args,
'kwargs': kwargs,
'result': result,
# ===========================================
# MY IMPLEMENTATION OF THE __repr___ PROPERTY
# ===========================================
'repr': (
f"{func.__name__}("
# 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})"
)
}
self.count = 1
return result
return wrapper
return log_outer
func_log = LoggerUtil()
@func_log.log()
def any_func(*args, **kwargs):
print(f"Positional arguments passed: {args}")
print(f"Keyword arguments passed: {kwargs}")
print('-' * 30, '\n')
return None
# ==============================
# TEST THE FUNCTION
# ==============================
any_func('pos_arg1', 'pos_arg2', start=0, stop=10, step=1)
# Prints to console:
# ``````````````````
# Positional arguments passed: ('pos_arg1', 'pos_arg2')
# Keyword arguments passed: {'start': 0, 'stop': 10, 'step': 1}
# ------------------------------
# ==============================
# DISPLAY THE LOGS
# ==============================
print(func_log.record, '\n')
# Prints to console: (with indentation added for clarity)
# ``````````````````
# {
# 0: {
# 'function': 'any_func',
# 'args': ('pos_arg1', 'pos_arg2'),
# 'kwargs': {'start': 0, 'stop': 10, 'step': 1},
# 'result': None,
# 'repr': "any_func('pos_arg1', 'pos_arg2', start=0, stop=10, step=1)"
# }
# }
# ==============================
# TEST THE repr PROPERTY
# ==============================
function_repr = func_log.record[0]['repr']
eval(function_repr)
# Prints to console:
# ``````````````````
# Positional arguments passed: ('pos_arg1', 'pos_arg2')
# Keyword arguments passed: {'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:
pos_args = func_log.record[0]['args']
kw_args = func_log.record[0]['kwargs']
func_name = func_log.record[0]['function']
# If I know the name of the function:
any_func(*pos_args, **kw_args)
# Or, more generally:
kw_args = str(kw_args)[1:-1].replace('\': ', '=').replace("'", '')
eval(
func_name
'('
str(pos_args)[1:-1]
','
str(kw_args)
')'
)
# Prints to the console:
# ``````````````````````
# Positional arguments passed: ('pos_arg1', 'pos_arg2')
# Keyword arguments passed: {'start': 0, 'stop': 10, 'step': 1}
# ------------------------------
# Positional arguments passed: ('pos_arg1', 'pos_arg2')
# Keyword arguments passed: {'start': 0, 'stop': 10, 'step': 1}
# ------------------------------
... 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: doesn't work 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:tuple(str(k} '=' str(v) for k, v in kwargs.items())
becomes('key1=val1', 'key2=val2')
instead of('key1=val1, key2=val2')
. - Appending
__repr__()
to the end of the function, like:func(*args, **kwargs).__repr__()
. This doesn't work because thefunc()
is evaluated first, so the.__repr__()
method just returns a string representation of the value returned byfunc()
(None
in the case of my example code above). Note that appending.__repr__()
to the function call is the same asrepr(func())
.
CodePudding user response:
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}