Of course! The symbol in Python is used for decorator syntax.
A decorator is a powerful and elegant feature that allows you to modify or extend the behavior of a function or class method without permanently modifying its source code. Think of it as "wrapping" a function with additional functionality.
The Core Concept: Functions as First-Class Citizens
To understand decorators, you first need to know that in Python, functions are first-class objects. This means:
- You can assign a function to a variable.
- You can pass a function as an argument to another function.
- You can return a function from another function.
This last point is the key to how decorators work.
A Simple Example Without Decorators
Imagine you want to add logging to a function every time it's called. Without a decorator, you might do this:
def add(a, b):
print(f"Function 'add' is being called with args: {a}, {b}")
return a + b
# Using the function
result = add(5, 3)
print(f"Result: {result}")
# Output:
# Function 'add' is being called with args: 5, 3
# Result: 8
This works, but what if you have 20 functions and you want to add logging to all of them? You'd have to copy-paste the print statement into every single function. This is repetitive and not maintainable.
How Decorators Work: The Manual Way
A decorator is a function that takes another function as an argument, adds some functionality, and returns a new function.
Let's create a my_decorator function that does the logging for us.
def my_decorator(func):
"""This is the decorator function."""
def wrapper():
"""This is the 'wrapper' function that replaces the original."""
print(f"Something is happening before the function '{func.__name__}' is called.")
result = func() # Call the original function
print(f"Something is happening after the function '{func.__name__}' is called.")
return result
return wrapper
def say_hello():
print("Hello!")
# Now, let's apply our decorator
say_hello = my_decorator(say_hello)
# Call the decorated function
say_hello()
# Output:
# Something is happening before the function 'say_hello' is called.
# Hello!
# Something is happening after the function 'say_hello' is called.
What happened here?
- We defined
my_decorator, which takes a functionfuncas an argument. - Inside
my_decorator, we defined awrapperfunction. Thiswrappercontains the extra code we want to run (the print statements). - The
wrapperfunction calls the originalfunc()and returns its result. my_decoratorreturns thiswrapperfunction.- The line
say_hello = my_decorator(say_hello)is crucial. It re-assigns thesay_hellovariable to be thewrapperfunction returned bymy_decorator. So, when you callsay_hello(), you are actually callingwrapper().
The Syntax (The "Syntactic Sugar")
The manual way works, but it's a bit clunky. Python provides a cleaner, more readable syntax using the symbol. This is called "syntactic sugar" because it makes the code sweeter.
The following two code blocks are functionally identical:
Without :
def my_decorator(func):
def wrapper():
print("Before...")
func()
print("After...")
return wrapper
def say_hello():
print("Hello!")
say_hello = my_decorator(say_hello)
With :
def my_decorator(func):
def wrapper():
print("Before...")
func()
print("After...")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
# Now you can just call the function directly
say_hello()
The @my_decorator line above the function definition is a shortcut that tells Python: "Instead of calling say_hello = my_decorator(say_hello), do it automatically."
Practical Examples
Example 1: Timing Function Execution
This is a very common use case. Let's create a decorator that measures how long a function takes to run.
import time
def timer_decorator(func):
def wrapper(*args, **kwargs): # *args and **kwargs make the decorator flexible
start_time = time.time()
result = func(*args, **kwargs) # Pass original arguments
end_time = time.time()
print(f"Function '{func.__name__}' executed in {end_time - start_time:.4f} seconds")
return result
return wrapper
@timer_decorator
def slow_function(seconds):
print(f"Sleeping for {seconds} seconds...")
time.sleep(seconds)
slow_function(2)
# Output:
# Sleeping for 2 seconds...
# Function 'slow_function' executed in 2.0021 seconds
Note: We used `argsand*kwargsto allow thewrapper` to accept any number of positional and keyword arguments, making our decorator much more reusable.
Example 2: Checking User Permissions
You can use decorators to enforce access control.
def admin_required(func):
def wrapper(user, *args, **kwargs):
if user.get('role') != 'admin':
print("Access Denied: Admins only.")
return
return func(user, *args, **kwargs)
return wrapper
@admin_required
def delete_user(user, username_to_delete):
print(f"Admin '{user['name']}' is deleting user '{username_to_delete}'.")
# --- Usage ---
current_user = {'name': 'Alice', 'role': 'admin'}
delete_user(current_user, 'Bob') # Works
regular_user = {'name': 'Charlie', 'role': 'user'}
delete_user(regular_user, 'Dave') # Blocked
# Output:
# Admin 'Alice' is deleting user 'Bob'.
# Access Denied: Admins only.
Decorators with Arguments
What if you want to pass arguments to the decorator itself? For example, a repeat decorator that repeats a function a certain number of times.
To do this, you need one more level of function nesting.
def repeat(num_times):
"""A decorator factory that takes an argument."""
def decorator_repeat(func):
"""The actual decorator."""
def wrapper(*args, **kwargs):
"""The wrapper function."""
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator_repeat
@repeat(num_times=3)
def greet(name):
print(f"Hello, {name}!")
greet("World")
# Output:
# Hello, World!
# Hello, World!
# Hello, World!
Here, repeat is a decorator factory. It's a function that returns a decorator. The @repeat(num_times=3) syntax first calls repeat(num_times=3), which returns the decorator_repeat function, which is then applied to greet.
Summary
| Concept | Description | Example |
|---|---|---|
| Decorator | A function that modifies another function. | @timer |
| Wrapper | The inner function in a decorator that contains the extra logic. | def wrapper(): ... |
| Syntax | Syntactic sugar for applying a decorator. Makes code cleaner. | @my_decorator |
| *`args, kwargs` | Used in a wrapper to accept any arguments, making the decorator flexible. | def wrapper(*args, **kwargs): ... |
| Decorator Factory | A function that returns a decorator, allowing you to pass arguments to the decorator. | @repeat(num_times=3) |
Decorators are a cornerstone of modern Python programming, used extensively in web frameworks (like Flask and Django for routing), logging, testing, and many other areas.
