
Python Decorators
Decorators in Python are a fascinating concept that might seem complex at first, but with some practice, they become an incredibly useful part of programming.
What are decorators? A decorator is a function that takes another function as input and returns a new function as output. Simply put, a decorator is a function that alters the behavior of another function or method. Decorators allow you to "decorate" or enhance functions with additional functionality in an elegant and readable manner.
This enables you to add features to an existing function without directly modifying it.
Let's explore what decorators are and how to use them.
Creating a Decorator
First, you need to create a decorator as a function.
def decorator_name(func):
def wrapper():
# Code to execute before the function
func()
# Code to execute after the function
return wrapper
Applying a Decorator to a Function
Once defined, you can add the decorator to another function by writing @decorator_name right before the function definition.
@decorator_name
def function():
print("This is my function!")
This syntax applies the decorator to the function, modifying its behavior according to the decorator's definition.
This way, you can add the same functionality to multiple functions without having to modify each one individually. Additionally, you can apply this functionality only when you need it. You can also use decorators with classes.
A Simple Example
Let's say we want to add an attribute 'lang' that prints "english" when called.
def mydecorator(func):
func.lang = 'english'
return func
Now define a function foo() with the decorator syntax @mydecorator.
@mydecorator
def foo():
pass
This way, the foo() function is decorated with the "lang" attribute.
If you now call the "lang" attribute from the foo() function, it returns "english".
print(foo.lang)
english
You can achieve the same result by decorating the function with this syntax:
foo = mydecorator(foo)
The final result is the same:
print(foo.lang)
english
You can also stack multiple decorators on the same function or method.
For example, create two decorators that each add a different attribute, "lang" and "version".
def mydecorator1(func):
func.lang = 'english'
return func
def mydecorator2(func):
func.version = '3.1'
return func
Then define the foo() function with both decorators.
@mydecorator1
@mydecorator2
def foo():
pass
The function is now decorated with both decorators.
This way, you can access the "lang" and "version" attributes from the same function.
print(foo.lang)
print(foo.version)
english
3.1
In this case, the order of decorators is important because it defines the application hierarchy.
For example, create two decorators that define the "lang" attribute.
def mydecorator1(func):
func.lang = 'italian'
return func
def mydecorator2(func):
func.lang = 'english'
return func
Then define the foo() function with both decorators.
@mydecorator1
@mydecorator2
def foo():
pass
In this case, foo is decorated first with mydecorator1 and then with mydecorator2.
If you now access the "lang" attribute, Python returns "italian" because it finds the attribute in the first decorator mydecorator1 and overrides the one in the second decorator mydecorator2.
print(foo.lang)
italian
Decorators with Parameters
Sometimes, it can be useful to have decorators that accept parameters. This adds a level of complexity but is very powerful.
Let's see an example:
def repeat(num_times):
def decorator_repeat(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator_repeat
Now you can use `repeat` with a parameter to repeat the function multiple times:
@repeat(num_times=3)
def greet(name):
print(f"Hello, {name}!")
When you call the function, it runs three times.
greet("Bob")
Hello, Bob!
Hello, Bob!
Hello, Bob!
Alternatively, you can define the decorated function using this syntax:
def greet(name):
print(f"Hello, {name}!")
greet2 = repeat(3)(greet)
This allows you to assign a different name to the decorated function compared to the original one.
greet2("Bob")
Hello, Bob!
Hello, Bob!
Hello, Bob!
Class Decorators
Decorators can also be applied to classes. A common example is the `@staticmethod` or `@classmethod` decorator in Python.
For instance, let's create a decorator that adds a `greet` method to a class:
def add_greet_method(cls):
def greet(self):
print("Welcome!")
cls.greet = greet
return cls
The add_greet_method() decorator takes a class `cls` as an argument and modifies it.
In this example, the decorator defines a greet() method that prints "Welcome!" and adds it to the `cls` class.
Now, let's apply this decorator to a simple `Person` class:
@add_greet_method
class Person:
def __init__(self, name):
self.name = name
Then, create an instance of the Person class:
p = Person("Alice")
Finally, call the new greet method:
p.greet()
Welcome!
This example provides a straightforward and practical explanation of how to use decorators with classes:
Now, let's consider a more complex example.
def add_class_method(cls):
cls.class_method = classmethod(lambda cls: print(f"This is a class method in {cls.__name__}"))
return cls
@add_class_method
class MyClass:
pass
Within the decorator, a class method is added to the class passed as an argument (cls).
This class method is defined using classmethod, a built-in Python function that converts a regular function into a class method.
In this case, the class method is defined using a lambda function. This function prints a message that includes the class name (cls.__name__).
After decorating MyClass, this class will have a new class method called class_method.
For example, access the class method:
MyClass.class_method()
This will print:
This is a class method in MyClass
How to Apply a Decorator to a Function Without Modifying It
Decorators are often used to enhance functions without altering them directly.
In such cases, we use a wrapper function. The decorator acts as a wrapper that encapsulates the target function, creating a new, decorated version of it.
For example, let's define a decorator that converts the result of the decorated function to its absolute value:
def absolute_result(f):
def wrapper(*args, **kwargs):
result = f(*args, **kwargs)
return abs(result)
return wrapper
Next, define a simple function that calculates the sum of two numbers without applying the decorator:
def add(a, b):
"""Calculates the sum of two numbers."""
return a + b
Now, apply the decorator without modifying the original function:
decorated_add = absolute_result(add)
This way, you can use both functions as needed.
For example, you can call the decorated function, which returns the absolute value of the sum:
print(decorated_add(-5, 3))
2
You can also call the original, undecorated function that returns the algebraic sum:
print(add(-5, 3))
-2
This approach allows you to apply a decorator to a function without directly altering it, keeping the original function intact.
Remember that the original function's metadata is not preserved in the wrapper function.
For instance, in this case, the metadata "decorated_add.__name__" and "decorated_add.__doc__" are not preserved.
print(decorated_add.__name__)
print(decorated_add.__doc__)
wrapper
None
You can only access this metadata directly from the original function:
print(add.__name__)
print(add.__doc__)
add
Calculates the sum of two numbers.
To preserve the original function's metadata, you need to use a different approach with functools.wraps.
Using functools.wraps for Wrapper Functions
To preserve the original function's metadata in the wrapper function, you can define it using the functools.wraps module.
For example, this wrapper function calculates the absolute value of another function's result:
import functools
def absolute_result(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
result = f(*args, **kwargs)
return abs(result)
return wrapper
Define a function that calculates the sum of two numbers:
def add(a, b):
"""Calculates the sum of two numbers."""
return a + b
Apply the decorator and assign the decorated function to a new variable:
decorated_add = absolute_result(add)
In this case, you can use both the decorated and the original function:
print(decorated_add(-5, -3))
8
However, in this case, the original function's metadata is preserved in the decorated function thanks to the use of functools.wraps:
print(decorated_add.__name__)
print(decorated_add.__doc__)
add
Calculates the sum of two numbers.
The original function's metadata remains intact:
print(add.__name__)
print(add.__doc__)
add
Calculates the sum of two numbers.
This approach allows you to apply a decorator to a function without altering it directly, preserving the original function and the metadata of the decorated function using functools.wraps.
In conclusion, decorators in Python are a highly useful tool for extending and modifying the behavior of functions and methods, even though they may seem complex at first.
Remember, the key to understanding decorators is knowing that they are simply functions that take another function as an argument and return a new function. Once you grasp this concept, you can create increasingly complex and useful decorators for your code.