Execution Timer Wrapper
A medium Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.
- Domain
- Python
- Difficulty
- medium
- Seniority
- L6
- Asked in
- Agoda
Problem
The jobs in JOBS are plain functions of different arity, each wrapped at definition by the make_timed decorator so its run time gets logged. Implement make_timed and run_timed, which runs the named job with the positional args it is given and hands back exactly what the job itself returned: the measured seconds are logged, never returned.
Function wrapped with a timer. Duration captured on exit.
Example 1
Input:
job = "answer"
Output:
42
Example 2
Input:
job = "add", args = [3,4]
Output:
7
Worked solution and explanation
What this is really asking
This is a transparency test wearing a profiler costume. The skill being probed: can you write a wrapper that sits between every caller and every job, serves any arity through *args, and leaves no fingerprint on the return value? Anyone can call time.perf_counter() twice. What separates candidates is where the elapsed seconds go. Fold them into the return as (result, elapsed) and 42 comes back as (42, 0.0001): every caller of every job in JOBS breaks at once.
Notice the blast radius. make_timed is applied at definition time with @make_timed, so the name add no longer points at the function you read; it points at whatever make_timed returned. A wrapper that leaks timing, or a make_timed that forgets return wrapper, poisons the whole registry before run_timed ever runs.
Building it
Step 1: Return a replacement, not a result
make_timed(func) defines an inner wrapper that closes over func and returns that wrapper. The decorator never calls the job itself; it builds a replacement that will.
Step 2: Forward whatever arrives
answer takes nothing, double takes one value, add takes two. One wrapper serves all of them only if it accepts *args, **kwargs and passes them straight through to func.
Step 3: Clock around the call, return the job's value
Read the clock, call func, read the clock again, and log the difference. Then return result, the exact object the job produced. The elapsed time is a side effect, never part of the contract.
Step 4: Dispatch by name in run_timed
run_timed treats a missing args as an empty list, looks the timed job up in JOBS by name, and spreads the arguments with *args. Calling timed_job(args) instead would hand add a single list and blow up on arity.
import time
def make_timed(func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.6f}s")
return result
return wrapper
@make_timed
def answer():
return 42
@make_timed
def double(n):
return n * 2
@make_timed
def add(x, y):
return x + y
@make_timed
def greet(greeting, name):
return greeting + name
@make_timed
def pair(a, b):
return [a, b]
JOBS = {
"answer": answer,
"double": double,
"add": add,
"greet": greet,
"pair": pair,
}
def run_timed(job, args=None):
if args is None:
args = []
timed_job = JOBS[job]
return timed_job(*args)- Leaky wrapper
return result, elapsedchanges every job's return type.add(3, 4)now yields(7, 0.00001)and every equality check downstream fails.- Transparent wrapper
print(...)thenreturn result.add(3, 4)still yields7; the timing lives in the log, where a profiler belongs.
Common follow-up questions
- After decorating, `add.__name__` reads 'wrapper'. How do you keep the original name and docstring? (Tests `functools.wraps` on the inner function.)
- What if a job raises? Should the time still be logged? (Tests wrapping the call in `try` / `finally`.)
- How would you turn `make_timed` into `make_timed(threshold)` that only logs slow runs? (Tests a decorator factory: one more level of closure.)