How to you get the information that a wrapper function returns when using a decorator function in Python?

Viewed 62

I am trying to access the time to execute a function that is inside a decorator function.

I followed the direction in this post because I was unable to get the function to take the arguments I passed to it. Now I am unsure of how to get the data from calling the function.

Here is my code:

import time
from functools import wraps

def sort_timer(func):
    def outer(func):
        @wraps(func)
        def inner(*args, **kwargs):
            start = time.perf_counter()
            func(*args, **kwargs)
            finish = time.perf_counter()
            return start - finish
        return inner
    return outer

@sort_timer
def bubble_sort(a_list):
    """Sorts a_list in ascending order"""
    for pass_num in range(len(a_list) - 1):
        for index in range(len(a_list) - 1 - pass_num):
            if a_list[index] > a_list[index + 1]:
                temp = a_list[index]
                a_list[index] = a_list[index + 1]
                a_list[index + 1] = temp

list1 = [60, 19, 22, 14, 43, 27, 3, 77]
x = bubble_sort(list1)
print(x)

It would appear that what is being returned is the inner function. Here is what is logged to the console:

<function sort_timer.<locals>.outer.<locals>.inner at 0x0000027167770310>

Any insight would be appreciated. Thank you.

3 Answers

The code in the answer to which you refer is specifically for supplying arguments to the decorator. It will work as long as you put () after @sort_timer... and, as currently written, provide an argument. Here's an example, with the first func parameter renamed for clarity:

def sort_timer(parameter):
    def outer(func):
        @wraps(func)
        def inner(*args, **kwargs):
            start = time.perf_counter()
            func(*args, **kwargs)
            finish = time.perf_counter()
            return start - finish
        return inner
    return outer

@sort_timer(13) # for example
...

That said, in your code as shown, you're not really using the double-wrapper technique to any advantage. Unless you plan on supplying arguments to the decorator itself (not the decorated function), you could simplify like so:

def sort_timer(func):
    @wraps(func)
    def inner(*args, **kwargs):
        start = time.perf_counter()
        func(*args, **kwargs)
        finish = time.perf_counter()
        return start - finish
    return inner

Why not using a simple decorator for timing like e.g. mentioned in timeit versus timing decorator ?

import time
import functools

def timeit(f):
    @functools.wraps(f)
    def timed(*args, **kw):

        ts = time.time()
        result = f(*args, **kw)
        te = time.time()

        print(f"func:{f.__name__} args:{args}{'' if len(kw) == 0 else kw} took: {(te-ts)*1000 :.3f} msec")
        return result

    return timed

@timeit
def bubble_sort(a_list):
    """Sorts a_list in ascending order"""
    for pass_num in range(len(a_list) - 1):
        for index in range(len(a_list) - 1 - pass_num):
            if a_list[index] > a_list[index + 1]:
                temp = a_list[index]
                a_list[index] = a_list[index + 1]
                a_list[index + 1] = temp

list1 = [60, 19, 22, 14, 43, 27, 3, 77]
x = bubble_sort(list1)

which prints:

func:bubble_sort args:([3, 14, 19, 22, 27, 43, 60, 77],) took: 0.029 msec

The @functools.wraps(f) takes f's meta information and copies it to the decorating function timed, so that the resulting decorated function keeps the features of the original/target function.

Here is the code for calculating execute time of a function:

import time
 
def timer(func):
  def wrapper(*args, **kwargs):
    start = time.time()
    func(*args, **kwargs)
    end = time.time() - start
    print(end)
  return wrapper
Related