0

So I'm making a timer wrapper function, but I keep getting errors when trying to print the arguments.

My timer function looks like this:

def time_me(fn):
    from time import time as t
    def wrapper(*args, **kwargs):
        start_time = t()
        fn(*args, **kwargs)
        end_time = t()
        print("executing {} with arguments {}, took: {}".format(fn.__name__, *args, end_time - start_time))
        return fn(*args, **kwargs)
    return wrapper

Ideally it'd print the kwargs as well, but apparently that's a whole other issue, although I'd much appreciate if someone helped me with that. So what I have works fine when there's noly one argument:

@time_me
def dummy_fn(N):
    from time import sleep as ts
    ts(0.2)
    return 

dummy_fn(1000)

gives me: executing dummy_fn with arguments 1000, took: 0.2128157615661621

But if I redefine dummy_fn to take more arguments and pass them through it still only prints the first. When I tried to convert my *args to string I got errors saying that "str() argument 2 must be str, not int". I don't seem to be able to access my *args using for example the list() function either.

Does anyone know how to solve this?

LegendWK
  • 184
  • 9

1 Answers1

2

Using *args in the function signature packs all of the positional arguments into a single tuple args. If you then say *args in another function call within your the function body, it spreads it back into multiple arguments, which you don't want -- e.g. it messes up your .format call because it's expecting 1 argument, and doing something like str(*args) fails because now you're passing multiple arguments to the str function. Just pass args as-is and it'll be formatted into a suitable string.

Note also that your wrapper calls the wrapped function twice, which you probably don't want (since then it's actually taking twice as long to run as what you measured, side effects are being repeated, etc). It should call it once (inside the timer), remember the result, and then return the result.

Here's a fixed version:

def time_me(fn):
    from time import time as t
    def wrapper(*args, **kwargs):
        start_time = t()
        result = fn(*args, **kwargs)
        end_time = t()
        print(f"executed {fn.__name__} with arguments {args} and {kwargs}, took: {end_time - start_time}")
        return result
    return wrapper

If you don't like the result = ... pattern, you could use a context manager:

from contextlib import contextmanager
from functools import wraps
from time import time

def time_me(fn):
    @contextmanager
    def timed_context(desc):
        start_time = time()
        try:
            yield
        finally:
            end_time = time()
            print(f"executed {desc}, took: {end_time - start_time}")

    @wraps(fn)
    def wrapper(*args, **kwargs):
        with timed_context(f"{fn.__name__} with arguments {args} and {kwargs}"):
            return fn(*args, **kwargs)

    return wrapper

(Why the @wraps(fn)? You already know about fn.__name__, but did you know that if you used .__name__ on the function decorated by time_me, it would now say wrapper instead of the original function? You can fix this with the functools.wraps decorator, which applies to a wrapper function and makes it "look like" the wrapped function.)

Samwise
  • 68,105
  • 3
  • 30
  • 44
  • Oh, can't believe I didn't notice that I was calling my function twice, I was sure the runs took longer than they said. Are there any downsides to using f-string printing as opposed to .format() or %? – LegendWK Mar 30 '21 at 08:51
  • 1
    f-strings are new in Python 3, so if you're writing for Python 2 you can't use them, but in a Python 3 codebase they're generally the preferred way of doing this type of formatting. – Samwise Mar 30 '21 at 14:05
  • 1
    note that if you had used f-strings originally and you did something like `f"{*args}"` you would get the very helpful error `SyntaxError: can't use starred expression here` right away instead of confusingly having it work for one argument but not more. :) – Samwise Mar 30 '21 at 14:13