Python decorators are an incredibly useful construct in Python. Using decorators in Python, we can modify the behavior of a function by wrapping it inside another function. Decorators enable us to write cleaner code and share functionality. This comprehensive guide will provide an in-depth explanation of not only how to use decorators but also how to create them.
What is a Python Decorator?
A Python decorator is a function that takes in another function as an argument, adds some kind of functionality to it, and returns a modified version of the original function.
In more technical terms, a decorator foo() is said to decorate a function bar() if:
foo()takesbar()as a parameterfoo()returns another callable functionbaz()baz()is a modified version ofbar()that contains additional logic
So foo() decorates bar() by returning baz() which has the same signature as bar() but enhanced functionality.
Here is some sample code to illustrate:
# Foo is a decorator, it takes in another function, bar as a parameter
def foo(bar):
# Here we create baz, a modified version of bar
# baz will call bar but can do anything before and after
def baz():
# Before calling bar, we print something
print("Something")
# Then we run bar by making a function call
bar()
# Then we print something else after running bar
print("Something else")
# Finally, foo returns baz, a modified version of bar
return baz
So in summary:
foo()is the decorator functionbar()is the original functionbaz()is the decorated version ofbar()returned byfoo()
This allows us to enhance bar() by wrapping it inside foo() which returns the enhanced baz().
How to Create a Decorator in Python
To illustrate how decorators are created and used in Python, let‘s walk through a simple example.
We will create a decorator function called create_logger() that will log the name of the function it decorates every time the decorated function is called.
Step 1: Define the Decorator Function
First, we need to define our decorator function. The decorator will take in the function we want to decorate as a parameter named func:
# Create the decorator function
def create_logger(func):
# Function body will go here
Step 2: Define the Enhanced Function
Inside our decorator, we will define a nested function called modified_func() that will contain the additional logging logic:
# Inside create_logger
def modified_func():
# Print function name
print("Calling: ", func.__name__)
# Call the function being decorated
func()
Step 3: Return the Enhanced Function
Finally, our decorator returns the enhanced function modified_func:
# Create the decorator
def create_logger(func):
# Define the enhanced function
def modified_func():
print("Calling: ", func.__name__)
func()
# Return it
return modified_func
And we have now created our create_logger() decorator function! It takes in a function func, enhances it by adding logging, and returns the modified version.
How to Use Decorators in Python
To use our new create_logger() decorator, we add @create_logger before the definition of the function we want to decorate:
@create_logger
def say_hello():
print("Hello, World!")
Now whenever we call say_hello(), it will first print the message "Calling: say_hello" before executing the actual function body.
Here is what happens when we run say_hello():
Calling: say_hello
Hello, World!
The @create_logger syntax allows us to easily decorate say_hello() with our decorator.
This is equivalent to manually calling the decorator as follows:
# Original function
def say_hello():
print("Hello, World!")
# Manually call decorator
say_hello = create_logger(say_hello)
So the @ syntax automatically applies the decorator function for us.
Slightly More Advanced Examples
The above example illustrated basic usage. Now let‘s look at some more advanced scenarios:
Decorating Functions with Arguments
If the function being decorated accepts arguments, then the wrapper function needs to accept arbitrary arguments and pass them when calling the wrapped function:
def foo(func):
# Wrapper accepts arbitary args
def wrapper(*args, **kwargs):
# Do something before
print("Before")
# Call function with received args
func(*args, **kwargs)
# Do something after
print("After")
return wrapper
For example:
@foo
def bar(x, y):
print(x, y)
bar(1, 2)
This will output:
Before
1 2
After
The wrapper has access to the arguments passed to the decorated function.
Decorating Classes
Python allows decorating classes too. The decorator will be applied to the __init__ method of the class:
def foo(func):
def wrapper(*args, **kwargs):
# Do something before init
print("Creating object!")
# Call init with received args
func(*args, **kwargs)
return wrapper
@foo
class Bar:
def __init__(self):
print("In init")
# Creating object!
# In init
This can be useful for initializing classes by doing something before __init__ is called.
Real-World Examples of Decorators in Python
While you can create custom decorators like shown above, Python also provides some built-in decorators like @staticmethod, @classmethod, @property etc.
Let‘s look at some common real-world examples of using decorators:
@staticmethod
The @staticmethod decorator is used to create static methods in a class. Static methods can be called on a class without needing to instantiate it:
class Calculator:
@staticmethod
def add(x, y):
return x + y
# Call static method
result = Calculator.add(2, 3)
Static methods can‘t access class state because they don‘t receive the class instance as first arg.
@classmethod
Similar to static methods, @classmethod can also be called on the class directly. But class methods receive the class as the first argument instead of an instance.
class Calculator:
@classmethod
def name(cls):
return cls.__name__
print(Calculator.name()) # Prints Calculator
This allows class methods to access class state.
@property
The @property decorator is used to create properties in Python classes. This allows accessing methods like attributes:
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
p = Person(‘John‘)
print(p.name) # Calls getter internally
We can also define setters using the @x.setter decorator to reassign values.
@name.setter
def name(self, new_name):
self._name = new_name
p.name = ‘Mary‘ # Calls setter
@lru_cache
Python provides a built-in @lru_cache decorator in its functools module. This implements memoization and caching to prevent expensive function calls from running repeatedly:
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
print(f"Computing fib({n})")
return n if n < 2 else fib(n-1) + fib(n-2)
print(fib(7)) # Computes once
print(fib(7)) # Returns cached value
The maxsize parameter sets the limit on cached results. This decorator can significantly speed up programs using recursion or dynamic programming.
@wraps
Sometimes decorators modify the metadata of the wrapped function such as its docstring or name. The @wraps decorator from functools module preserves these metadata:
from functools import wraps
def foo(func):
@wraps(func)
def wrapper():
# Do something
return func()
return wrapper
@foo
def bar():
"""bar docstring"""
pass
print(bar.__name__) # bar
print(bar.__doc__) # bar docstring
This ensures that the wrapped function‘s metadata is preserved by the decorator.
Why are Decorators Important in Python?
Now that you have seen decorators in action, let‘s discuss why they are so useful in Python:
Enables Code Reuse
Decorators allow logic to be reused easily. The same decorator can be applied to multiple functions to avoid repetition and share functionality. For example, a caching decorator can easily be applied to compute-heavy functions to speed them up.
Cleaner and Modular Code
Decorators help divide code into smaller reusable pieces that focus on single concerns. The business logic stays in the function while cross-cutting concerns are handled separately by decorators. This makes code modular and cleaner.
Extend Functions without Modification
Decorators extend behavior of functions dynamically without needing to modify them. New features can be added to existing functions by applying decorators. This improves maintainability and reduces risk.
Implement Common Patterns
Many design patterns like logging, rate-limiting, authentication etc can be implemented cleanly using decorators. This avoids cluttering business logic with these cross-cutting concerns.
Widely Used in Python Libraries and Frameworks
Decorators enable libraries and frameworks like Flask, Django, pytest etc to provide extensibility. For example, Flask uses decorators to define routes, handle errors and secure views. Decorators are a natural fit for implementing middleware behaviors.
Overall, decorators make it easy to modify behavior on the fly and are integral to Python architecture and design. Mastering their usage is key to writing clean and Pythonic code.
Final Thoughts
We have covered a lot of ground on how decorators work in Python and how you can create and use them. Here are some key takeaways:
- Decorators are functions that modify and extend other functions.
- They allow logic to be added before and after calls to decorated functions.
- Python provides some built-in decorators like
@property,@staticmethodetc. - Custom decorators can be defined easily in Python by using nested wrapper functions.
- Decorators are widely used in Python frameworks and enable cleaner code by separating concerns.
Decorators open up many possibilities for metaprogramming in Python. They are also powerful tools for implementing design patterns and behaviors like caching, logging, rate-limiting etc.
I hope this guide gave you a good understanding of how to use decorators effectively. They will become invaluable parts of your Python toolkit allowing you to write more modular and maintainable code.
Next you can read up on Python context managers which are another powerful construct in Python for resource management.