Python decorators: what it is, how they work and why you need them

онлайн тренажер по питону
Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

What Are Decorators in Python?

If you've been writing Python for more than a week, you've probably seen the @ symbol above a function. This is a decorator — a powerful and clear programming tool.

Decorators in Python allow you to "wrap" one function around another. They modify or extend the behavior of a function without changing its source code. Decorators are used everywhere: in Flask, Django, FastAPI, pytest, and even in the Python standard library.

How Decorators Work

Decorator Basics

A decorator is a function that takes another function and returns a new function or a modified version of the old one.

def decorator(func):
    def wrapper():
        print("Before call")
        func()
        print("After call")
    return wrapper

@decorator
def say_hello():
    print("Hello!")

say_hello()

Execution result:

Before call
Hello!
After call

The @decorator symbol is syntactic sugar for:

say_hello = decorator(say_hello)

Inner Workings of Decorators

To understand decorators in Python, you need to know the key concepts:

  • In Python, functions are first-class objects.
  • Functions can be passed as arguments to other functions.
  • Other functions can be defined inside functions.
  • There is a closure mechanism.

Universal Decorator Template

The minimum decorator template looks like this:

def decorator(func):
    def wrapper(*args, **kwargs):
        # Actions before function execution
        result = func(*args, **kwargs)
        # Actions after function execution
        return result
    return wrapper

Practical Examples of Decorators

Execution Time Measurement Decorator

import time

def timing(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        execution_time = time.time() - start
        print(f"{func.__name__} took {execution_time:.4f} sec")
        return result
    return wrapper

@timing
def slow_func():
    time.sleep(1)
    return "Operation completed"

slow_func()

This decorator is useful for profiling function performance and optimizing code.

Authorization Check Decorator

def require_admin(func):
    def wrapper(user, *args, **kwargs):
        if user != "admin":
            raise PermissionError("Access denied")
        return func(user, *args, **kwargs)
    return wrapper

@require_admin
def delete_database(user):
    print("Database deleted!")
    return True

delete_database("admin")  # Successful execution

This decorator ensures application security by controlling access to critical functions.

Logging Decorator

import logging
from functools import wraps

def log_calls(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        logging.info(f"Calling function {func.__name__} with arguments {args}, {kwargs}")
        try:
            result = func(*args, **kwargs)
            logging.info(f"Function {func.__name__} executed successfully")
            return result
        except Exception as e:
            logging.error(f"Error in function {func.__name__}: {e}")
            raise
    return wrapper

Decorators with Arguments

Creating a Decorator Factory

When a decorator needs to accept parameters, a decorator factory is created:

def repeat(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            results = []
            for _ in range(n):
                result = func(*args, **kwargs)
                results.append(result)
            return results
        return wrapper
    return decorator

@repeat(3)
def greet():
    print("Hello!")
    return "Greeting"

greet()

Decorator with Conditional Parameters

def cache_result(ttl_seconds=300):
    def decorator(func):
        cache = {}
        def wrapper(*args, **kwargs):
            key = str(args) + str(kwargs)
            if key in cache:
                cached_time, cached_result = cache[key]
                if time.time() - cached_time < ttl_seconds:
                    return cached_result
            
            result = func(*args, **kwargs)
            cache[key] = (time.time(), result)
            return result
        return wrapper
    return decorator

Built-in Python Decorators

Main Built-in Decorators

Python provides several built-in decorators for working with classes and functions:

  • @staticmethod — creates a static class method that does not require an instance.
  • @classmethod — creates a class method that takes the class as the first argument cls.
  • @property — converts a method into an attribute with getter, setter, and deleter capabilities.
  • @functools.lru_cache — caches function results to improve performance.

Example of Using @property

class Temperature:
    def __init__(self, celsius=0):
        self._celsius = celsius
    
    @property
    def celsius(self):
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature cannot be below absolute zero")
        self._celsius = value
    
    @property
    def fahrenheit(self):
        return (self._celsius * 9/5) + 32

Saving Function Metadata

Using functools.wraps

To preserve the function name, documentation, and other attributes of the original function, you need to use the @functools.wraps decorator:

from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling function: {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def important_function():
    """This is an important function."""
    return "Result"

print(important_function.__name__)  # important_function
print(important_function.__doc__)   # This is an important function.

Without @wraps, the function name would become wrapper, and the documentation would be lost.

Applying Decorators in Popular Frameworks

Django

In Django, decorators are widely used to control access and handle HTTP requests:

from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods

@login_required
@require_http_methods(["GET", "POST"])
def dashboard(request):
    return render(request, 'dashboard.html')

Flask

Flask uses decorators for routing and handling requests:

from flask import Flask
app = Flask(__name__)

@app.route('/home')
@app.route('/dashboard')
def home():
    return "Welcome!"

@app.before_request
def before_request():
    print("Executed before each request")

pytest

In pytest, decorators are used to parametrize tests and configure the test environment:

import pytest

@pytest.mark.parametrize("a,b,expected", [
    (1, 2, 3),
    (2, 3, 5),
    (10, 5, 15)
])
def test_sum(a, b, expected):
    assert a + b == expected

@pytest.fixture
def sample_data():
    return {"key": "value"}

Using Multiple Decorators

Decorator Application Order

When using multiple decorators, they are applied from bottom to top:

@decorator_a
@decorator_b
@decorator_c
def func():
    pass

Equivalent to the notation:

func = decorator_a(decorator_b(decorator_c(func)))

Practical Example of Multiple Decorators

def bold(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return f"<b>{result}</b>"
    return wrapper

def italic(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return f"<i>{result}</i>"
    return wrapper

@bold
@italic
def say_hello():
    return "Hello!"

print(say_hello())  # <b><i>Hello!</i></b>

Recommendations for Using Decorators

Best Practices

When working with decorators, follow these principles:

  • Always use @functools.wraps to preserve function metadata.
  • Apply *args and **kwargs for decorator universality.
  • Avoid creating overly complex nested decorators.
  • Don't overuse decorators with side effects.
  • Remember that decorators are applied during module import, not when the function is called.

Testing Functions with Decorators

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Decorator worked")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def calculate(x, y):
    return x + y

# To test the original function
original_calculate = calculate.__wrapped__
assert original_calculate(2, 3) == 5

Common Mistakes and Solutions

Main Errors When Working with Decorators

  • Problem of losing function metadata.
    • Solution: use @functools.wraps.
  • Incorrect handling of arguments.
    • Solution: use *args and **kwargs in the wrapper function.
  • Errors in decorators with arguments.
    • Solution: create the correct decorator factory with three levels of nesting.
  • Performance issues.
    • Solution: cache results and avoid redundant calculations in the decorator.

Debugging Decorated Functions

import functools

def debug_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"DEBUG: Calling {func.__name__} with args={args}, kwargs={kwargs}")
        try:
            result = func(*args, **kwargs)
            print(f"DEBUG: {func.__name__} returned {result}")
            return result
        except Exception as e:
            print(f"DEBUG: {func.__name__} raised exception {e}")
            raise
    return wrapper

Conclusion

Decorators in Python are a powerful tool for extending functionality without changing the source code of functions. They are widely used in modern development for solving problems of logging, caching, authorization, performance measurement, and many others.

Proper use of decorators makes the code cleaner, more modular, and reusable. By mastering the principles of creating and applying decorators, the developer gains access to elegant solutions for many everyday programming tasks.

Frequently Asked Questions

  • What is a decorator in Python? A decorator is a function that takes another function as an argument and returns a modified version of that function, extending or changing its behavior.
  • How does the @ symbol work in Python? The @ symbol represents syntactic sugar for applying a decorator. The @decorator notation is equivalent to func = decorator(func).
  • Can I pass arguments to a decorator? Yes, to do this, a decorator factory is created — a function that takes arguments and returns a decorator: @decorator(arg).
  • Why do I need @functools.wraps? The @functools.wraps decorator preserves the name, documentation, and other attributes of the original function, preventing them from being lost during decoration.
  • Do decorators belong to functional programming? Yes, decorators are part of the functional programming style in Python, as they work with functions as first-class objects.

News