Decorators
Decorators let you change or add extra behavior to a function or class without actually touching its original code.
They’re used a lot in real projects for things like:
- Logging
- Authentication
- Caching
- Validation
- Tracking how fast something runs
A Simple Way to Picture It
Section titled “A Simple Way to Picture It”Think of a decorator as something that wraps around a function, like a layer on top of it.
graph TD
A[Call Function] --> B[Decorator Wrapper]
B --> C[Run Code Before]
C --> D[Original Function]
D --> E[Run Code After]
E --> F[Return Result]
The Basic Idea
Section titled “The Basic Idea”A decorator is really just:
- A function
- That takes another function as input
- And gives back a new function
Step-by-step structure
Section titled “Step-by-step structure”def decorator(func): def wrapper(): # before func() # after return wrapperA Basic Example
Section titled “A Basic Example”import functools
def my_decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): print("Before function runs") result = func(*args, **kwargs) print("After function runs") return result
return wrapper
@my_decoratordef greet(): print("Hello!")
greet()What’s Actually Happening Behind the Scenes
Section titled “What’s Actually Happening Behind the Scenes”greet = my_decorator(greet)Why functools.wraps Matters
Section titled “Why functools.wraps Matters”Without using wraps, you lose some important details, like:
- The function’s name
- Its docstring
- The help info that shows up when you look it up
import functools
def my_decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper
@my_decoratordef greet(): """This function greets the user""" print("Hello")
print(greet.__name__) # greetprint(greet.__doc__) # This function greets the userHandling Arguments the Right Way
Section titled “Handling Arguments the Right Way”A good decorator should be able to work with any function, no matter what arguments it takes.
That’s why we use:
*args # positional arguments**kwargs # keyword argumentsExample
Section titled “Example”import functools
def log_calls(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") result = func(*args, **kwargs) print(f"{func.__name__} finished") return result
return wrapper
@log_callsdef add(a, b): return a + b
print(add(2, 3))Decorators That Take Their Own Arguments
Section titled “Decorators That Take Their Own Arguments”Sometimes you want to customize how a decorator behaves.
To do that, you need to add one more layer around it.
Structure
Section titled “Structure”def decorator_with_args(config): def decorator(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decoratorExample: Running Something More Than Once
Section titled “Example: Running Something More Than Once”import functools
def repeat(times): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): result = None for _ in range(times): result = func(*args, **kwargs) return result
return wrapper
return decorator
@repeat(3)def greet(name): print(f"Hello {name}")
greet("Asha")Decorators on Classes
Section titled “Decorators on Classes”Decorators can be used on classes too, not just functions.
They take the class as input and can make changes to it.
def add_status(cls): cls.status = "ready" return cls
@add_statusclass Task: pass
print(Task.status) # readyWhere You’ll Actually Use This in Real Projects
Section titled “Where You’ll Actually Use This in Real Projects”Some common uses:
- Keeping a log of function calls
- Checking if someone has permission to do something
- Measuring how long something takes to run
- Saving (caching) results so you don’t have to calculate them again
- Checking that inputs are valid
Example: Measuring How Long a Function Takes
Section titled “Example: Measuring How Long a Function Takes”import functoolsimport time
def time_it(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds") return result
return wrapper
@time_itdef slow_function(): time.sleep(1)
slow_function()