Batu Lab NotesPractical developer guides

Write a timing decorator that preserves a function name

By Batu ยท English technical notes

Also published in our Blogger archive.

Write a timing decorator that preserves a function name

A timing decorator often hides the name of the function it wraps because the returned callable is named wrapper. functools.wraps copies selected metadata from the wrapped function and sets __wrapped__, so tools and logs can still identify add as add.

This recipe injects clock into timed instead of reading a system clock inside the decorator. StepClock returns 10.000 before add(2, 3) and 10.125 afterward, making the measured result exactly 0.125 seconds. The decorated call returns a pair: the original integer result, 5, and the elapsed float. The assertions separately check the arithmetic, the deterministic elapsed value, the preserved __name__, and access to the undecorated function.

In production, inject time.perf_counter for a suitable monotonic elapsed-time clock; its output should not be asserted as an exact duration. This wrapper records no duration when the wrapped function raises, because its second clock call is after the call. Add try/finally and an error-reporting policy if failed calls must be measured. wraps improves metadata, but the runtime wrapper still accepts *args and **kwargs.

from functools import wraps


class StepClock:
    def __init__(self, readings):
        self._readings = iter(readings)

    def __call__(self):
        return next(self._readings)


def timed(clock):
    def decorate(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            started = clock()
            result = function(*args, **kwargs)
            elapsed = clock() - started
            return result, elapsed

        return wrapper

    return decorate


@timed(StepClock([10.000, 10.125]))
def add(left, right):
    return left + right


value, elapsed = add(2, 3)
assert value == 5
assert elapsed == 0.125
assert add.__name__ == "add"
assert add.__wrapped__(2, 3) == 5
print(f"{add.__name__} {value} {elapsed:.3f}")

Expected stdout:

add 5 0.125

By Batu. AI assistance was used to prepare this article.

Source: Python functools.wraps documentation.