Need to log execution time for several functions? Putting timing code inside each function repeats the same work. A decorator moves that shared behavior into one callable and applies it with `@` above each function definition. The decorated function can keep doing its original job while gaining the extra behavior. section 7.3 section 7.3.1
Listing 7.8 Using a performance-logging decorator
import random
import time
def logging_time(func):
def logger(*args, **kwargs):
print(f"--- {func.__name__} starts")
start_t = time.time()
value_returned = func(*args, **kwargs)
end_t = time.time()
print(f"*** {func.__name__} ends; used time: {end_t - start_t:.2f} s")
return value_returned
return logger
@logging_time
def example_func2():
random_delay = random.randint(3, 5) * 0.1
time.sleep(random_delay)
example_func2()
# output the following two lines:
--- example_func2 starts
*** example_func2 ends; used time: 0.40 s
The timing example prints a start message, calls the original function, prints elapsed time, and returns the function’s result. section 7.3.1
A function decorator takes a function and returns a replacement callable. Here, the returned inner function closes over `func`, forwards positional and keyword arguments, and returns the original result. Omitting that return would silently change the decorated function’s behavior; defining an inner function with no parameters would fail when the decorated function receives arguments. section 7.3.2
Add a call announcement
```python import functools def announce(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper @announce def add(a, b): return a + b print(add(2, 3)) # Calling add # 5 ``` The announcement lives in one place; `add` still receives its arguments and produces its result. section 7.3.1 section 7.3.2 section 7.3.3
Decoration binds the function’s name to the returned wrapper. Without `functools.wraps(func)`, inspecting `__name__` or `__doc__` can reveal the wrapper’s metadata instead of the original function’s. Use `wraps` on the inner function when defining a function decorator. section 7.3.3 section 7.3.4
Listing 7.9 Wrapping the decorated function
import functools
def logging_time_wraps(func):
@functools.wraps(func)
def logger(*args, **kwargs):
"""Log the time"""
print(f"--- {func.__name__} starts")
start_t = time.time()
value_returned = func(*args, **kwargs)
end_t = time.time()
print(f"*** {func.__name__} ends; used time:
{end_t - start_t:.2f} s")
return value_returned
return logger
@logging_time_wraps
def example_wraps():
"""Example function"""
pass
print(example_wraps.__doc__, example_wraps.__name__, sep="; ")
# output: Example function; example_wraps
Compare the wrapper and its printed `__doc__` and `__name__` to see how `functools.wraps` preserves the decorated function’s metadata. section 7.3.3
A decorator can also be a class. Its constructor accepts the function, and an instance’s `__call__` handles later calls; the decorated name then refers to a callable instance rather than a function. A different built-in use, `@dataclass`, uses annotated fields to generate class boilerplate such as `__init__`. For a practical timing exercise, decorate data-manipulation methods and compare CSV-file and SQLite approaches. section 10.5.4 section 9.2.2 section 14.3.7






