← Назад к курсу

Декораторы в Python: Полное руководство

📌 Что такое декоратор?

Декоратор — это функция, которая принимает другую функцию и расширяет её поведение, не изменяя её исходный код. Это реализация паттерна "Декоратор" для функций и методов.

Базовый принцип работы:

def decorator(func):          # 1. Принимает функцию
    def wrapper():            # 2. Создает обертку
        # Дополнительная логика ДО
        result = func()       # 3. Вызывает оригинальную функцию
        # Дополнительная логика ПОСЛЕ
        return result
    return wrapper           # 4. Возвращает обертку

🎯 1. Синтаксис декораторов

Синтаксис с @ (сахар)

@decorator
def my_function():
    return "Hello"

# Эквивалентно:
def my_function():
    return "Hello"
my_function = decorator(my_function)

Пример в действии:

def make_uppercase(func):
    def wrapper():
        original_result = func()
        modified_result = original_result.upper()
        return modified_result
    return wrapper

@make_uppercase
def greet():
    return "hello world!"

print(greet())  # HELLO WORLD!

🔧 2. Декораторы с аргументами функции

Декоратор для функций с любыми аргументами:

def trace(func):
    def wrapper(*args, **kwargs):
        print(f"Вызов {func.__name__} с args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} вернула {result}")
        return result
    return wrapper

@trace
def add(a, b):
    return a + b

@trace
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(add(2, 3))
# Вызов add с args=(2, 3), kwargs={}
# add вернула 5
# 5

print(greet("Alice", greeting="Hi"))
# Вызов greet с args=('Alice',), kwargs={'greeting': 'Hi'}
# greet вернула Hi, Alice!
# Hi, Alice!

🎭 3. Декораторы с собственными аргументами

Декоратор с параметрами (двойная обертка):

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

@repeat(num_times=3)
def say_hello(name):
    print(f"Hello, {name}!")

say_hello("Bob")
# Hello, Bob!
# Hello, Bob!
# Hello, Bob!

📝 4. Сохранение метаданных функции

Проблема:

def decorator(func):
    def wrapper():
        """Wrapper docstring"""
        return func()
    return wrapper

@decorator
def original():
    """Original docstring"""
    pass

print(original.__name__)  # wrapper
print(original.__doc__)   # Wrapper docstring

Решение с functools.wraps:

from functools import wraps

def decorator(func):
    @wraps(func)  # Копирует метаданные из func в wrapper
    def wrapper():
        """Wrapper docstring"""
        return func()
    return wrapper

@decorator
def original():
    """Original docstring"""
    pass

print(original.__name__)  # original
print(original.__doc__)   # Original docstring

🏗️ 5. Декораторы классов

Декоратор как класс:

class Counter:
    def __init__(self, func):
        self.func = func
        self.count = 0
        # Важно: используем wraps для сохранения метаданных
        functools.update_wrapper(self, func)
    
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Вызов #{self.count} функции {self.func.__name__}")
        return self.func(*args, **kwargs)

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

say_hello()  # Вызов #1 функции say_hello
say_hello()  # Вызов #2 функции say_hello

Декоратор с параметрами через класс:

class Delay:
    def __init__(self, seconds):
        self.seconds = seconds
    
    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            import time
            print(f"Ждем {self.seconds} секунд...")
            time.sleep(self.seconds)
            return func(*args, **kwargs)
        return wrapper

@Delay(seconds=2)
def slow_function():
    print("Функция выполнена")

slow_function()

🔄 6. Цепочка декораторов (несколько декораторов)

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

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

@bold
@italic
def greet(name):
    return f"Hello, {name}!"

print(greet("Alice"))  # <b><i>Hello, Alice!</i></b>

# Порядок выполнения снизу вверх:
# 1. Сначала применяется @italic
# 2. Затем @bold к результату
# Эквивалентно: bold(italic(greet))

💼 7. Практические примеры декораторов

1. Тайминг выполнения:

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.perf_counter()
        result = func(*args, **kwargs)
        end_time = time.perf_counter()
        print(f"{func.__name__} выполнилась за {end_time - start_time:.6f} секунд")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "Done"

slow_function()  # slow_function выполнилась за 1.001234 секунд

2. Кэширование (мемоизация):

from functools import lru_cache

# Встроенный декоратор для кэширования
@lru_cache(maxsize=32)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# Свой декоратор кэша:
def cache(func):
    cache_dict = {}
    @wraps(func)
    def wrapper(*args):
        if args in cache_dict:
            print(f"Возвращаем из кэша: {args}")
            return cache_dict[args]
        result = func(*args)
        cache_dict[args] = result
        print(f"Вычисляем и кэшируем: {args}")
        return result
    return wrapper

3. Проверка прав доступа:

def requires_admin(func):
    @wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.get('is_admin', False):
            raise PermissionError("Требуются права администратора")
        return func(user, *args, **kwargs)
    return wrapper

@requires_admin
def delete_user(admin, username):
    print(f"Пользователь {username} удален")

admin_user = {'name': 'Alice', 'is_admin': True}
regular_user = {'name': 'Bob', 'is_admin': False}

delete_user(admin_user, "charlie")  # OK
delete_user(regular_user, "charlie")  # PermissionError

4. Валидация аргументов:

def validate_types(*arg_types, **kwarg_types):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Проверяем позиционные аргументы
            for arg, expected_type in zip(args, arg_types):
                if not isinstance(arg, expected_type):
                    raise TypeError(f"Ожидался {expected_type}, получен {type(arg)}")
            
            # Проверяем именованные аргументы
            for key, value in kwargs.items():
                if key in kwarg_types and not isinstance(value, kwarg_types[key]):
                    raise TypeError(f"{key}: ожидался {kwarg_types[key]}, получен {type(value)}")
            
            return func(*args, **kwargs)
        return wrapper
    return decorator

@validate_types(int, int, result=int)
def add(a, b):
    return a + b

print(add(1, 2))  # 3
print(add("1", 2))  # TypeError

5. Повтор при ошибке (retry):

import time
from functools import wraps

def retry(max_attempts=3, delay=1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    attempts += 1
                    if attempts == max_attempts:
                        raise
                    print(f"Попытка {attempts} не удалась: {e}. Повтор через {delay} сек")
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

@retry(max_attempts=3, delay=2)
def unreliable_function():
    import random
    if random.random() < 0.7:
        raise ValueError("Временная ошибка")
    return "Успех"

🧪 8. Декораторы для методов класса

Декоратор для методов:

def class_decorator(method):
    @wraps(method)
    def wrapper(self, *args, **kwargs):
        print(f"Вызов метода {method.__name__} класса {self.__class__.__name__}")
        return method(self, *args, **kwargs)
    return wrapper

class MyClass:
    @class_decorator
    def say_hello(self):
        print("Hello from MyClass!")
    
    @class_decorator
    def calculate(self, x, y):
        return x + y

obj = MyClass()
obj.say_hello()  # Вызов метода say_hello класса MyClass

Декоратор @property:

class Circle:
    def __init__(self, radius):
        self._radius = radius
    
    @property
    def radius(self):
        print("Получаем радиус")
        return self._radius
    
    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("Радиус должен быть положительным")
        print(f"Устанавливаем радиус: {value}")
        self._radius = value
    
    @property
    def area(self):
        return 3.14159 * self._radius ** 2

circle = Circle(5)
print(circle.radius)  # Получаем радиус → 5
circle.radius = 10     # Устанавливаем радиус: 10
print(circle.area)     # 314.159

🔍 9. Как работают встроенные декораторы

@staticmethod:

class MathUtils:
    @staticmethod
    def add(a, b):
        return a + b
    
    # Без декоратора:
    # def add(a, b):
    #     return a + b
    # add = staticmethod(add)

print(MathUtils.add(2, 3))  # 5
# Не получает self/cls, вызывается от класса

@classmethod:

class Person:
    total_people = 0
    
    def __init__(self, name):
        self.name = name
        Person.total_people += 1
    
    @classmethod
    def get_total(cls):
        return cls.total_people
    
    @classmethod
    def from_string(cls, string):
        # Альтернативный конструктор
        name = string.split(",")[0]
        return cls(name)

person = Person.from_string("John, Doe")
print(Person.get_total())  # 1

@abstractmethod:

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def make_sound(self):
        pass
    
    @abstractmethod
    def move(self):
        pass

class Dog(Animal):
    def make_sound(self):
        return "Woof!"
    
    def move(self):
        return "Running"

# Animal()  # TypeError: нельзя создать экземпляр абстрактного класса
dog = Dog()  # OK

🎨 10. Продвинутые техники

Декоратор с состоянием:

def call_counter(func):
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        print(f"Функция {func.__name__} вызвана {wrapper.calls} раз")
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper

@call_counter
def function():
    pass

function()  # Функция function вызвана 1 раз
function()  # Функция function вызвана 2 раз

Декоратор для синглтона:

def singleton(cls):
    instances = {}
    @wraps(cls)
    def wrapper(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return wrapper

@singleton
class Database:
    def __init__(self):
        print("Инициализация базы данных")

db1 = Database()  # Инициализация базы данных
db2 = Database()  # (ничего не выводится)
print(db1 is db2)  # True

Асинхронный декоратор:

import asyncio
from functools import wraps

def async_timer(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        start_time = asyncio.get_event_loop().time()
        result = await func(*args, **kwargs)
        end_time = asyncio.get_event_loop().time()
        print(f"{func.__name__} выполнилась за {end_time - start_time:.2f} секунд")
        return result
    return wrapper

@async_timer
async def fetch_data():
    await asyncio.sleep(1)
    return "Данные"

# asyncio.run(fetch_data())

📊 11. Отладка и тестирование декораторов

Декоратор для отладки:

def debug(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"Вызов {func.__name__}({signature})")
        
        result = func(*args, **kwargs)
        
        print(f"{func.__name__!r} вернула {result!r}")
        return result
    return wrapper

Тестирование декораторов:

import unittest

def test_decorator():
    """Тест для проверки работы декоратора"""
    calls = []
    
    def decorator(func):
        def wrapper(*args, **kwargs):
            calls.append((args, kwargs))
            return func(*args, **kwargs)
        return wrapper
    
    @decorator
    def func(x, y=10):
        return x + y
    
    assert func(1) == 11
    assert func(2, y=20) == 22
    assert len(calls) == 2
    print("Тест пройден!")

🎯 12. Итог: когда использовать декораторы

Ситуация Пример декоратора
Логирование @log_execution
Тайминг @timer
Кэширование @lru_cache
Проверка прав @login_required
Валидация @validate_input
Повтор при ошибке @retry
Транзакции БД @transaction
Кэширование веб-страниц @cache_page

Плюсы:

  • Переиспользуемость кода
  • Разделение ответственности
  • Читаемость
  • Неинвазивность (не меняем исходный код)

Минусы:

  • Усложняют отладку
  • Могут скрывать поведение
  • Медленнее вызова обычной функции
  • Могут затруднять чтение стека вызовов

💡 Советы для собеседования

  1. Объясняйте на примерах - покажите код
  2. Знайте разницу между @staticmethod, @classmethod, @property
  3. Помните про functools.wraps - важно сохранять метаданные
  4. Расскажите про паттерн "Декоратор" в ООП
  5. Приведите практические примеры из своего опыта
  6. Объясните, как работают цепочки декораторов

Ключевые моменты:

  • Декораторы изменяют поведение функций без изменения их кода
  • Это функции высшего порядка (принимают/возвращают функции)
  • Синтаксис @decorator - это синтаксический сахар
  • @wraps сохраняет метаданны оригинальной функции
  • Можно создавать декораторы с параметрами

Декораторы — мощный инструмент Python, который часто используется в веб-фреймворках (Django, Flask), библиотеках и production-коде для cross-cutting concerns.