← Назад к курсу
Декораторы в 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 |
Плюсы:
- Переиспользуемость кода
- Разделение ответственности
- Читаемость
- Неинвазивность (не меняем исходный код)
Минусы:
- Усложняют отладку
- Могут скрывать поведение
- Медленнее вызова обычной функции
- Могут затруднять чтение стека вызовов
💡 Советы для собеседования
- Объясняйте на примерах - покажите код
- Знайте разницу между @staticmethod, @classmethod, @property
- Помните про functools.wraps - важно сохранять метаданные
- Расскажите про паттерн "Декоратор" в ООП
- Приведите практические примеры из своего опыта
- Объясните, как работают цепочки декораторов
Ключевые моменты:
- Декораторы изменяют поведение функций без изменения их кода
- Это функции высшего порядка (принимают/возвращают функции)
- Синтаксис @decorator - это синтаксический сахар
- @wraps сохраняет метаданны оригинальной функции
- Можно создавать декораторы с параметрами
Декораторы — мощный инструмент Python, который часто используется в веб-фреймворках (Django, Flask), библиотеках и production-коде для cross-cutting concerns.