Published: October 26, 2023
Decorators in Python are a powerful and versatile feature that allows you to modify or enhance functions or methods in a clean and readable way. They are a form of metaprogramming, meaning they are code that manipulates other code.
At its core, a decorator is a callable that takes a function as an argument and returns a new function. This new function typically wraps the original function, adding some behavior before or after the original function is executed, or even replacing it entirely.
The syntax for applying a decorator is the @decorator_name syntax placed directly above the function definition.
Let's start with a basic decorator that logs when a function is called:
def log_function_call(func):
def wrapper(*args, **kwargs):
print(f"Calling function: {func.__name__}")
result = func(*args, **kwargs)
print(f"Function {func.__name__} finished.")
return result
return wrapper
@log_function_call
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
When you run this code, you'll see output like:
Calling function: greet
Hello, Alice!
Function greet finished.
The @log_function_call syntax is syntactic sugar for the following:
def greet(name):
print(f"Hello, {name}!")
greet = log_function_call(greet)
The log_function_call decorator takes the greet function, creates a wrapper function that adds logging, and then reassigns the name greet to this new wrapper function. So, when you call greet("Alice"), you are actually calling the wrapper function.
*args and **kwargs: These are crucial for making decorators flexible enough to handle functions with any number of positional and keyword arguments.functools.wraps: It's best practice to use @functools.wraps(func) inside your decorator's wrapper function. This preserves the original function's metadata (like its name, docstring, etc.), which is important for introspection and debugging.functools.wraps
import functools
def log_function_call_with_wraps(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling function: {func.__name__}")
result = func(*args, **kwargs)
print(f"Function {func.__name__} finished.")
return result
return wrapper
@log_function_call_with_wraps
def say_goodbye(name):
"""A simple function to say goodbye."""
print(f"Goodbye, {name}!")
say_goodbye("Bob")
print(f"Docstring of say_goodbye: {say_goodbye.__doc__}")
print(f"Name of say_goodbye: {say_goodbye.__name__}")
Without @functools.wraps, the docstring and name would belong to the wrapper function.
You can also create decorators that accept arguments. This requires an extra layer of nesting:
import functools
def repeat(num_times):
def decorator_repeat(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator_repeat
@repeat(num_times=3)
def say_whee():
print("Whee!")
say_whee()
This decorator repeat takes an argument num_times and returns another decorator that will apply the function num_times.
Python decorators are a powerful tool for adding reusable functionality to your functions and methods without altering their core logic. By understanding closures and the @ syntax, you can write cleaner, more maintainable, and more expressive Python code.