Decorators and Context Managers
Decorators
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.time() - start:.4f}s")
return result
return wrapper
@timer
def slow_task():
time.sleep(1)
slow_task()
A decorator wraps a function to add behavior around it — logging, timing, permission checks, caching — without changing the function’s own code. The @timer syntax above is shorthand for slow_task = timer(slow_task); the decorator receives the original function, and returns a new function (wrapper) that does extra work before and after calling it.
*args and **kwargs
Notice wrapper(*args, **kwargs) in the example above — this lets the decorator work on any function, regardless of how many positional or keyword arguments it takes. *args collects any number of positional arguments into a tuple; **kwargs collects any number of keyword arguments into a dictionary.
Decorators from the standard library
from functools import lru_cache
@lru_cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
@lru_cache is a real, widely-used decorator from Python’s standard library — it automatically remembers previous results, so calling fibonacci(30) a second time returns instantly instead of recalculating from scratch. This is the exact same mechanism as the custom @timer decorator above, just already written for you.
Writing your own context manager
class Timer:
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, *args):
print(f"Elapsed: {time.time() - self.start:.4f}s")
with Timer():
time.sleep(1)
__enter__ and __exit__ are what make the with statement from the File Handling lesson work — you’re now able to build your own resource-managing objects, not just use built-in ones like open(). __exit__ runs even if an exception is raised inside the with block, which is exactly why with open(...) reliably closes a file even when something goes wrong while reading it.