Decorators in Python
Today we'll explore one of Python's most powerful and elegant features — decorators. They allow you to modify or enhance functions and methods without changing their source code! Think of decorators as special wrappers that add extra functionality to your code. 🎁
What are Decorators?
A decorator is a special kind of function that takes another function as input, extends or modifies its behavior, and returns the altered function without changing its source code.
Decorators follow a key principle in programming — the Open/Closed Principle, which states that code should be open for extension but closed for modification. With decorators, you can add new behaviors to functions without altering their original code.
Basic Decorator Syntax
The basic syntax for using a decorator is very simple — you place the @decorator_name line right above the function definition:
>>> def my_decorator(func): ... def wrapper(): ... print("Something happens before the function is called") ... func() # Call the original function ... print("Something happens after the function is called") ... return wrapper >>> @my_decorator >>> def say_hello(): ... print("Hello, world!") # Call the decorated function >>> say_hello()
Something happens before the function is calledHello, world!Something happens after the function is called
This is equivalent to:
>>> def my_decorator(func): ... def wrapper(): ... print("Something happens before the function is called") ... func() ... print("Something happens after the function is called") ... return wrapper >>> def say_hello(): ... print("Hello, world!") # Decorate the function manually >>> decorated_hello = my_decorator(say_hello) # Call the decorated function >>> decorated_hello()
Something happens before the function is calledHello, world!Something happens after the function is called
Decorators with Arguments
What if the function you're decorating takes arguments? You need to make your wrapper function accept and pass these arguments:
>>> def my_decorator(func): ... def wrapper(*args, **kwargs): ... print("Something happens before the function is called") ... result = func(*args, **kwargs) # Call the original function with all arguments ... print("Something happens after the function is called") ... return result ... return wrapper >>> @my_decorator >>> def add(a, b): ... return a + b # Call the decorated function with arguments >>> result = add(5, 3) >>> print(f"Result: {result}")
Something happens before the function is calledSomething happens after the function is calledResult: 8
The *args and **kwargs syntax allows the wrapper to accept any number of positional and keyword arguments, making it flexible enough to work with any function.
Practical Examples of Decorators
Let's explore a practical example of a decorator to see how it can be useful in real code.
Timing Functions
A decorator to measure how long a function takes to execute:
>>> import time >>> def timing_decorator(func): ... def wrapper(*args, **kwargs): ... start_time = time.time() ... result = func(*args, **kwargs) ... end_time = time.time() ... execution_time = end_time - start_time ... print(f"Function {func.__name__} took {execution_time:.4f} seconds to execute") ... return result ... return wrapper >>> @timing_decorator >>> def calculate_sum(n): ... return sum(range(n)) >>> calculate_sum(1000000)
Function calculate_sum took 0.0462 seconds to execute
Decorators with Parameters
Sometimes you need decorators that can take their own parameters. This requires adding another level of nesting:
>>> def repeat(n=1): ... def decorator(func): ... def wrapper(*args, **kwargs): ... result = None ... for _ in range(n): ... result = func(*args, **kwargs) ... return result ... return wrapper ... return decorator >>> @repeat(n=3) >>> def say_hi(name): ... print(f"Hi, {name}!") ... return "Done" >>> say_hi("Alice")
Hi, Alice!Hi, Alice!Hi, Alice!
In this example, we have three levels of functions:
- repeat(n) — takes the decorator parameter
- decorator(func) — takes the function to be decorated
- wrapper(*args, **kwargs) — handles the function call
Chaining Multiple Decorators
You can apply multiple decorators to a single function. They're executed from bottom to top (the decorator closest to the function is applied first):
>>> def bold(func): ... def wrapper(*args, **kwargs): ... return f"<b>{func(*args, **kwargs)}</b>" ... return wrapper >>> def italic(func): ... def wrapper(*args, **kwargs): ... return f"<i>{func(*args, **kwargs)}</i>" ... return wrapper >>> @bold >>> @italic >>> def format_text(text): ... return text >>> print(format_text("Hello, world!"))
<b><i>Hello, world!</i></b>
Class Decorators
In addition to decorating functions, you can also create decorators for classes:
>>> def add_greeting(cls): ... # Add a new method to the class ... cls.greet = lambda self: f"Hello from {self.__class__.__name__}" ... return cls >>> @add_greeting >>> class Person: ... def __init__(self, name): ... self.name = name # Create an instance and use the added method >>> person = Person("Alice") >>> print(person.greet())
Hello from Person
Real-World Use Cases for Decorators
Decorators are widely used in Python and many popular frameworks:
-
Flask/Django routing — map URLs to view functions
@app.route('/home') def home(): return "Welcome to the home page!"
-
Property decorators — control access to attributes
class Person: def __init__(self, name): self._name = name @property def name(self): return self._name @name.setter def name(self, value): if not value: raise ValueError("Name cannot be empty") self._name = value
-
Memoization/caching — store function results to avoid repetitive calculations
def memoize(func): cache = {} def wrapper(*args): if args not in cache: cache[args] = func(*args) return cache[args] return wrapper
-
Rate limiting — limit how often a function can be called
-
Validation — validate inputs before processing
-
Authorization — check permissions before executing functions
Understanding Check
What is the primary purpose of decorators in Python?
Conclusion
Decorators are a powerful feature in Python that allow for elegant code organization and reuse. They help implement cross-cutting concerns like logging, authentication, and performance monitoring without cluttering the actual business logic of your functions.
As you continue your Python journey, you'll find decorators are an essential tool for writing clean, maintainable, and extensible code. They're especially prevalent in Python frameworks and libraries, so understanding how they work will make you more effective in using these tools.