Что изменится, если декоратор применить к classmethod снаружи, а не к исходной функции внутри?
Декоратор метода следует применять к исходной функции внутри classmethod: сначала оборачивать функцию, затем превращать результат в метод класса. Если декоратор окажется снаружи, он получит объект-дескриптор classmethod, а не обычную функцию, поэтому типичная обёртка может потерять корректную передачу cls или завершиться ошибкой при вызове.
В Python методы класса реализованы через дескрипторы: специальный объект при доступе через класс или экземпляр формирует вызываемый метод и автоматически передаёт класс как первый аргумент. classmethod решает задачу отделения метода от экземпляра, сохраняя привязку к самому классу.
Декораторы появились как способ декларативно изменять функции во время создания класса или модуля. Когда декоратор применяется к методу, он должен учитывать, что после создания функции Python может дополнительно обернуть её дескриптором classmethod, staticmethod или property.
Порядок декораторов определяет, какой объект получает каждый декоратор. В корректном варианте пользовательский декоратор получает функцию и возвращает функцию-обёртку, после чего classmethod превращает эту обёртку в дескриптор метода класса.
При обратном порядке пользовательский декоратор получает уже объект classmethod. Простая обёртка, которая пытается вызвать сохранённый объект как функцию, обычно приводит к TypeError, поскольку её ожиданием была обычная функция, а не дескриптор. Даже если конкретный декоратор умеет работать с дескрипторами, неправильный порядок усложняет сохранение исходной семантики.
Рассмотрим два порядка:
Сначала log_calls получает функцию create и возвращает обычную функцию wrapper. Затем classmethod оборачивает wrapper в дескриптор. При обращении User.create("Ada") дескриптор передаёт User в wrapper как первый аргумент, поэтому исходная функция получает корректный cls.
При записи @log_calls над @classmethod порядок вычисления обратный: сначала создаётся classmethod, затем этот объект передаётся в log_calls. Универсальная обёртка вызовет его как func(*args, **kwargs), но объект classmethod предназначен прежде всего для связывания через атрибут класса, а не для такого прямого вызова.
functools.wraps сохраняет метаданные функции, но не исправляет порядок дескрипторов и не превращает classmethod обратно в обычную функцию. Если декоратор должен поддерживать разные дескрипторы, это нужно проектировать отдельно: распознавать их, извлекать исходную функцию и возвращать дескриптор соответствующего типа.
Аналогичная осторожность нужна со staticmethod и property, но правила их связывания различаются. Поэтому безопасный общий принцип таков: сначала применяйте декоратор, работающий с функцией, а затем специальный встроенный декоратор, формирующий дескриптор.
В ORM-модели был метод-фабрика, вызываемый как Model.from_record(data). Команда добавила внешний декоратор аудита поверх classmethod, рассчитывая, что он будет работать с любым вызываемым объектом. В тестах вызов завершился ошибкой: обёртка получила дескриптор, но пыталась вызвать его напрямую.
Рассматривались два варианта. Можно было усложнить аудиторский декоратор поддержкой classmethod, staticmethod и других дескрипторов, но это увеличивало его связанность с механизмом классов и число особых случаев. Можно было перенести аудит внутрь classmethod, чтобы декоратор всегда работал с обычной функцией.
Выбрали второй вариант: сначала оборачивать функцию аудитом, затем применять classmethod. Решение сохранило автоматическую передачу cls, упростило декоратор и сделало порядок применения очевидным для остальных методов фабрики.
cls в пользовательский декоратор автоматически?Нет. Декоратор вызывается при создании класса и получает объект функции, но не получает конкретный класс и не участвует в последующем связывании метода. cls добавляется дескриптором classmethod позже, когда метод извлекают через класс или экземпляр.
Поэтому обёртка должна принимать первый позиционный аргумент и передавать его дальше. Если декоратор объявляет обёртку без *args или иным образом меняет сигнатуру, вызов classmethod может завершиться ошибкой уже независимо от самого classmethod.
functools.wraps неправильный порядок декораторов?Нет. wraps копирует метаданные, такие как имя, документацию и ссылку __wrapped__, но не меняет тип оборачиваемого объекта и не восстанавливает дескрипторное поведение.
Если внешний декоратор получил classmethod, wraps лишь скопирует доступные атрибуты этого объекта в обёртку или может столкнуться с несовместимыми атрибутами. Для корректной поддержки дескрипторов требуется логика самого декоратора, а не только сохранение метаданных.
Нет. Это зависит от реализации декоратора и версии Python. Специализированный декоратор может явно распознавать classmethod, работать с его исходной функцией и возвращать новый classmethod.
Однако обычный декоратор, рассчитанный на функции, не должен получать дескриптор снаружи. Практический стандарт — располагать такие декораторы под @classmethod; это минимизирует зависимость от деталей реализации и сохраняет ожидаемую семантику связывания.