Decorators are a powerful feature in Python that allow you to modify the behavior of functions or methods. They provide a flexible way to extend and enhance your code without permanently modifying it. In this tutorial, we'll explore how to use decorators, including function decorators with the @ syntax, using functools.wraps, decorators with arguments, class decorators, and chaining decorators.
Decorators are essentially functions that take another function as an argument and extend its behavior without explicitly modifying it. They are a great tool for adding functionality to existing code in a clean and reusable way. In Python, decorators can be applied using the @ syntax, which makes them easy to read and apply.
A decorator is a function that takes another function as an argument and returns a new function. The simplest form of a decorator uses the @ syntax to wrap a function.
Let's start with a basic example where we create a simple decorator that prints a message before calling the original function.
1def my_decorator(func):2def wrapper():3print("Something is happening before the function is called.")4func()5print("Something is happening after the function is called.")6return wrapper78@my_decorator9def say_hello():10print("Hello!")1112say_hello()
Something is happening before the function is called. Hello! Something is happening after the function is called.
In this example, my_decorator is a decorator that takes a function func as an argument. Inside my_decorator, we define a nested function wrapper that adds some behavior before and after calling func. The @my_decorator syntax is syntactic sugar for say_hello = my_decorator(say_hello).
When you create a decorator, the metadata of the original function (like its name and docstring) is lost. To preserve this information, you can use the functools.wraps decorator.
1import functools23def my_decorator(func):4@functools.wraps(func)5def wrapper():6print("Something is happening before the function is called.")7func()8print("Something is happening after the function is called.")9return wrapper1011@my_decorator12def say_hello():13"""Prints a greeting."""14print("Hello!")1516print(say_hello.__name__) # Output: say_hello17print(say_hello.__doc__) # Output: Prints a greeting.
say_hello Prints a greeting.
Using functools.wraps ensures that the original function's metadata is preserved, which is important for debugging and documentation.
Decorators can also take arguments. To achieve this, you need to define an outer function that takes the decorator arguments and returns a decorator.
Here's an example of a decorator that measures the execution time of a function:
1import time2import functools34def timer(func):5@functools.wraps(func)6def wrapper(*args, **kwargs):7start_time = time.time()8result = func(*args, **kwargs)9end_time = time.time()10print(f"{func.__name__} executed in {end_time - start_time:.4f} seconds")11return result12return wrapper1314@timer15def compute_sum(n):16"""Computes the sum of numbers from 1 to n."""17return sum(range(1, n + 1))1819print(compute_sum(1000000))
compute_sum executed in 0.0256 seconds 499999500000
In this example, the timer decorator takes a function func as an argument and returns a new function wrapper. The wrapper function measures the time taken to execute func and prints it.
Decorators can also be applied to classes. A class decorator is a function that takes a class as an argument and returns a modified version of that class.
Let's create a simple class decorator that adds an attribute to a class.
1def add_attribute(cls):2cls.new_attribute = "Added by decorator"3return cls45@add_attribute6class MyClass:7pass89print(MyClass.new_attribute) # Output: Added by decorator
Added by decorator
In this example, add_attribute is a class decorator that adds an attribute new_attribute to the class it decorates.
Decorators can be chained together, meaning you can apply multiple decorators to a single function. The order of application matters, as each decorator modifies the behavior of the previous one.
Let's create two simple decorators and chain them together.
1def first_decorator(func):2@functools.wraps(func)3def wrapper():4print("First decorator")5func()6return wrapper78def second_decorator(func):9@functools.wraps(func)10def wrapper():11print("Second decorator")12func()13return wrapper1415@first_decorator16@second_decorator17def greet():18print("Hello!")1920greet()
First decorator Second decorator Hello!
In this example, the greet function is decorated with both first_decorator and second_decorator. The order of application is from bottom to top, so second_decorator is applied first, followed by first_decorator.
Let's put all these concepts together in a practical example. We'll create a decorator that logs the execution time of a function and another that adds an attribute to a class.
1import time2import functools34def timer(func):5@functools.wraps(func)6def wrapper(*args, **kwargs):7start_time = time.time()8result = func(*args, **kwargs)9end_time = time.time()10print(f"{func.__name__} executed in {end_time - start_time:.4f} seconds")11return result12return wrapper1314def add_attribute(cls):15cls.new_attribute = "Added by decorator"16return cls1718@timer19def compute_sum(n):20"""Computes the sum of numbers from 1 to n."""21return sum(range(1, n + 1))2223@add_attribute24class MyClass:25pass2627print(compute_sum(1000000))28print(MyClass.new_attribute)
compute_sum executed in 0.0256 seconds 499999500000 Added by decorator
In this example, we use the timer decorator to measure the execution time of the compute_sum function and the add_attribute class decorator to add an attribute to the MyClass class.
| Concept | Description |
|---|---|
| Function Decorators | Functions that modify other functions without permanently changing them. |
| @ Syntax | Syntactic sugar for applying decorators. |
| functools.wraps | Preserves the metadata of the original function. |
| Decorators with Args | Decorators that take arguments and return a decorator. |
| Class Decorators | Functions that modify classes by adding attributes or methods. |
| Chaining Decorators | Applying multiple decorators to a single function in a specific order. |
Now that you have a solid understanding of decorators, the next step is to learn about Python modules, packages, and PIP. Modules allow you to organize your code into reusable components, while packages help manage dependencies and distribute your code. PIP is the package installer for Python, which makes it easy to install and manage third-party libraries.
In the next tutorial, we'll explore how to create, use, and distribute Python modules and packages using PIP. Stay tuned!