So my task is: Write a decorator that logs information about calls of decorated functions, the values of its arguments, keyword arguments, and execution time. The log should be written to a file.
My solution is:
``
from time import time
def log(func):
def wrapper(*args, **kwargs):
start_time = time()
func(*args, **kwargs)
end_time = time() - start_time
with open('log.txt', 'a ') as file:
file.write(f'{func.__name__}; args: {args}, kwargs: {kwargs}, execution time: {end_time}\n sec.')
return wrapper
``
I have to test it with a function:
``
@log
def foo(a, b, c):
...
foo(1, 2, c=3)
``
And expected outcome in log.txt file must look something like this:
...
foo; args: a=1, b=2; kwargs: c=3; execution time: 0.12 sec.
...
But what I get is:
some_fun; args: (1, 2), kwargs: {'c': 3}, execution time: 0.0
sec.
How should I change my code to get an expected outcome?
CodePudding user response:
You could use signature
from the inspect
module (standard library) to fetch the variable names, and then use some string manipulations to get the desired output:
from inspect import signature
from time import perf_counter
def log(func):
params = signature(func).parameters.keys()
logmsg = func.__name__ "; args: {}; kwargs: {}; execution time: {} sec.\n"
def wrapper(*args, **kwargs):
start_time = perf_counter()
func(*args, **kwargs)
extime = perf_counter() - start_time
with open("log.txt", "a ") as file:
args = ", ".join(f"{name}={value}" for name, value in zip(params, args))
kwargs = ", ".join(f"{name}={value}" for name, value in kwargs.items())
file.write(logmsg.format(args, kwargs, extime))
return wrapper
perf_counter
is better for performance measurement.
(In case you're not aware: Your wrapper builds a function that only returns None
. If func
returns something that the wrapped func
should also return, then use result = func(*args, **kwargs)
and do return result
at the end.)
As pointed out by @zvone: This doesn't capture default arguments. If that is a concern, then you could use the following adjustment (which makes it a bit more complicated):
from inspect import signature
from time import perf_counter
def log(func):
sig = signature(func)
logmsg = func.__name__ "; args: {}; kwargs: {}; execution time: {} sec.\n"
def wrapper(*args, **kwargs):
start_time = perf_counter()
func(*args, **kwargs)
extime = perf_counter() - start_time
with open("log.txt", "a ") as file:
binds = sig.bind(*args, **kwargs)
binds.apply_defaults()
pairs = list(binds.arguments.items())
args_str = ", ".join(f"{n}={v}" for n, v in pairs[:len(args)])
kwargs_str = ", ".join(f"{n}={v}" for n, v in pairs[len(args):])
file.write(logmsg.format(args_str, kwargs_str, extime))
return wrapper
Here the default arguments that aren't given in args
or kwargs
get added to kwargs
with their default value (only for the purpose of logging).
CodePudding user response:
For now i think it's not possible, because what are you doing is displaying full {kwargs} and {args}, and they will only return a list.
Solution for this is, that you will write a for loop and display each item from {args} and {kwargs}, and for time, you shoul use another function not
start_time = time();
find other function in the python docs: https://docs.python.org/3/library/time.html#module-time