Декораторы: функции, оборачивающие функции
core написать простой декоратор и применить его для сквозной функциональности (например, логирования вызовов) вместо копипасты кода в каждую функцию
В C сквозная функциональность — логирование входа/выхода, замер времени, проверка прав — решается обычно одним из двух способов:
- Макрос препроцессора — текстовая подстановка до компиляции: мощно, но небезопасно и плохо отлаживается.
- Функция-обёртка — её нужно писать вручную, синхронно поддерживать при изменении сигнатуры оригинала и не забывать вызывать вместо оригинала.
void original_function(int arg) { /* ... */ }
void logged_function(int arg) {
printf("Calling with arg: %d\n", arg);
original_function(arg);
printf("Function finished.\n");
}
logged_function(42); // легко по ошибке вызвать original_function
В Python функции — объекты первого класса: их можно передавать как аргументы, возвращать из других функций, хранить в переменных. Декоратор — это функция, которая принимает функцию и возвращает другую функцию, обычно оборачивающую первую дополнительным поведением. Синтаксис @decorator над def — чистый синтаксический сахар:
@log_calls
def compute(x, y):
return x + y
# полностью эквивалентно:
def compute(x, y):
return x + y
compute = log_calls(compute)
Никакой магии: декоратор вызывается один раз, в момент определения функции, и имя compute после этого связано с тем объектом, который вернул log_calls.
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Вызов {func.__name__} с {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} вернула {result}")
return result
return wrapper
Здесь wrapper — замыкание, захватившее func (см. предыдущий урок про LEGB). *args, **kwargs позволяют оборачивать функцию любой сигнатуры без переписывания декоратора под каждый набор параметров — то, что в C потребовало бы либо variadic-функций с va_list и всей их хрупкостью, либо отдельного макроса под каждую сигнатуру.
Важная деталь: без functools.wraps обёрнутая функция «представляется» как wrapper — её __name__ и __doc__ берутся у обёртки. functools.wraps копирует __name__, __doc__ и другие метаданные с оригинала на wrapper; без этого интроспекция (help(), логи, отладка) сбивает с толку.
Декораторы — идиоматичный способ реализовать сквозные задачи в стиле DRY. Стандартная библиотека активно их использует: functools.lru_cache для кэширования, staticmethod и classmethod при описании методов классов.
graph TD
A["def compute(...) с @log_calls"] --> B["вызывается log_calls(compute)"]
B --> C["создаётся wrapper (замыкание над оригиналом)"]
C --> D["оригинальная compute доступна как func внутри wrapper"]
B --> E["имя compute теперь указывает на wrapper"]sequenceDiagram
participant Caller as Вызывающий код
participant Wrapper as wrapper (декоратор)
participant Orig as compute (оригинал)
Caller->>Wrapper: compute(x, y)
Wrapper->>Wrapper: логирование / проверка
Wrapper->>Orig: func(x, y)
Orig-->>Wrapper: результат
Wrapper-->>Caller: результатРазобранный пример
Допустим, пять функций нужно защитить проверкой прав доступа. В C-стиле проверка дублируется в начале каждой:
int compute(int x) {
if (!check_permission()) return -1;
/* основная логика */
}
И это дублирование пришлось бы повторить в каждой из пяти функций, синхронно поддерживая при изменении логики проверки.
В Python декоратор выносит проверку один раз:
import functools
def require_permission(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if not check_permission():
raise PermissionError(f"Нет доступа к {func.__name__}")
return func(*args, **kwargs)
return wrapper
@require_permission
def compute(x):
return x * 2
@require_permission
def delete_record(record_id):
...
Логика проверки существует в одном месте, применяется декларативно через @, а изменение правил не требует трогать код compute или delete_record — в отличие от явного вызова вспомогательной функции в каждом месте (риск забыть) или макроса, теряющего читаемость при отладке.
Попробуй сейчас
Напишите декоратор @timed, который замеряет время выполнения функции через time.perf_counter() и печатает "{имя функции} выполнилась за {время} сек". Не забудьте functools.wraps. Примените его к функции с искусственной задержкой (time.sleep(0.1)) и убедитесь, что в выводе — настоящее имя функции, а не wrapper.
Дополнительно: напишите декоратор check_positive, который проверяет, что все аргументы из *args и **kwargs.values() — числа больше нуля, и иначе бросает ValueError. Примените к calculate_area(width, height).
Получилось, если…
Вы разобрались, если можете написать декоратор с нуля (принимающий функцию, возвращающий wrapper с *args, **kwargs), объяснить назначение functools.wraps и назвать разницу между декоратором и C-макросом: декоратор — обычный код, выполняемый при определении функции и работающий с объектами-функциями, а не текстовая подстановка препроцессора до компиляции.
Вывод
Декоратор — функция, принимающая функцию и возвращающая функцию; @decorator — сахар для f = decorator(f), выполняемый один раз при определении. *args, **kwargs и functools.wraps — обязательный минимум для универсального и «прозрачного» декоратора.
AI-generated · source-grounded review
🛡 Fact-checked: 0 risky claims verified · 0 removed · confidence: high · figures: 2
A second model re-checked this lesson's claims and corroborated 0 of 3 , and could not confirm 3 either way. This is incomplete corroboration, not a disagreement.
On your own course these buttons answer instantly, quizzes track what you've mastered, and lessons adapt to your gaps. Write my course