Когда можно оборачивать класс декоратором в Python?
Что проверяют
Понимаете ли вы, что декоратор класса это обычная функция: она получает готовый объект класса и возвращает то, что окажется под его именем. Следом почти всегда спрашивают, чем это отличается от метакласса и __init_subclass__.
Как отвечать
Механика: @deco class A: ... равносильно A = deco(A). Декоратор выполняется один раз, когда класс уже полностью создан (отработали тело, метакласс, __set_name__, __init_subclass__), обычно в момент импорта модуля.
Уместен он, когда нужно доработать один конкретный класс или зарегистрировать его:
- генерация методов:
@dataclassпишет__init__,__repr__,__eq__по аннотациям,@functools.total_orderingдостраивает сравнения из__eq__и одного метода порядка вроде__lt__; - проверки при определении:
@enum.uniqueпадает на дублирующихся значениях сразу при импорте; - регистрация: плагины, обработчики команд, сериализаторы складываются в словарь-реестр, а класс возвращается без изменений;
- маркеры для инструментов:
@typing.final,@runtime_checkableу протоколов.
Дальше покажите границу. Декоратор действует только на тот класс, над которым написан: наследники получат уже добавленные методы, но сам декоратор к ним повторно не применится. Если логика должна срабатывать для каждого подкласса, проще __init_subclass__, а метакласс нужен, когда меняется само создание класса. Стек декораторов применяется снизу вверх, как у функций.
Хороший декоратор возвращает тот же класс или его подкласс, а не функцию-обёртку. Тогда isinstance, наследование, pickle и подсказки типов продолжают работать.
Где спотыкаются
Забывают return cls. Имя класса после этого указывает на None, и ошибка всплывает далеко от места, где её сделали.
Делают «синглтон» декоратором, который возвращает функцию с кэшем экземпляра. От такого «класса» нельзя наследоваться, isinstance(obj, Service) падает с TypeError, а статические анализаторы теряют тип.
Ожидают, что декоратор родителя сработает для потомков. Если он навешивает регистрацию, наследник в реестр не попадёт, для этого нужен __init_subclass__.