Программирование PythonМодель данныхРазработчик библиотек Python

Как Python обрабатывает подписывание класса квадратными скобками, если в классе определён class getitem ?

Как Python обрабатывает подписывание класса квадратными скобками, если в классе определён __class_getitem__?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

При подписывании класса Python вызывает его метод __class_getitem__, передавая класс и значение внутри квадратных скобок. Результат этого метода полностью определяется реализацией: это может быть объект-описание типа, псевдоним, схема или другое значение.

Это относится именно к подписыванию самого класса. Для подписывания экземпляра используется протокол __getitem__, а не __class_getitem__.

Исторический контекст

__class_getitem__ появился как механизм поддержки параметризации классов, прежде всего для обобщённых типов и аннотаций. Он позволил пользовательским классам реагировать на запись вроде Контейнер[Тип] без обязательного использования отдельной инфраструктуры для каждого класса.

В Python 3.7 этот механизм был добавлен в рамках поддержки более гибкой работы с обобщениями. Начиная с Python 3.9 стандартные коллекции получили возможность использовать синтаксис параметризации, например list[int], что сделало такой протокол особенно заметным в аннотациях типов.

Постановка проблемы

Класс иногда нужно воспринимать не только как вызываемый объект, но и как фабрику параметризованных описаний. Например, библиотека может предоставлять Ответ[int] и Ответ[str] как разные схемы, не создавая экземпляры Ответ.

Ошибочное смешение __class_getitem__ и __getitem__ приводит к неверному выбору метода. В результате подписывание класса может завершиться TypeError, а логика, предназначенная для экземпляров, неожиданно не будет вызвана.

Подробное решение

При выражении вида Класс[аргумент] Python сначала учитывает протокол подписывания метакласса. Если у метакласса есть подходящий __getitem__, приоритет получает он. Иначе Python использует __class_getitem__, определённый у самого класса.

Метод __class_getitem__ получает класс в параметре cls и значение подписывания в параметре item. Он вызывается автоматически как метод класса, поэтому отдельное применение @classmethod обычно не требуется.

class Box: def __class_getitem__(cls, item): return (cls.__name__, item) alias = Box[int] print(alias) # ('Box', <class 'int'>)

В примере Box[int] не создаёт экземпляр Box. Метод возвращает обычный кортеж, но на практике результатом обычно становится объект, описывающий параметризованный тип или схему.

obj[key] работает иначе: Python ищет __getitem__ у типа экземпляра obj. Поэтому наличие __class_getitem__ у класса не делает его экземпляры подписываемыми.

Реализация должна учитывать допустимые аргументы и определять понятное поведение для ошибочных. Если метод принимает только один тип, но получает несколько параметров или неподдерживаемое значение, лучше явно отклонить их, чем создать некорректное описание.

Важно также не путать параметризацию класса с его вызовом. Box[int] обычно возвращает описание или алиас, а Box() вызывает механизм создания экземпляра через __new__ и __init__.

Ситуация из практики

Библиотека сериализации предоставляет модели данных и хочет поддержать аннотации вроде Страница[Пользователь]. Рассматривались два варианта: обрабатывать такую запись внешней функцией или реализовать __class_getitem__ в базовом классе моделей.

Внешняя функция была проще для первоначальной реализации, но ухудшала читаемость аннотаций и требовала отдельного соглашения для каждой модели. Реализация через __class_getitem__ естественно связывает параметризацию с самой моделью, однако требует аккуратно валидировать аргументы и кэшировать созданные описания, если их создание дорого.

Был выбран __class_getitem__: он возвращает отдельный неизменяемый объект схемы с исходным классом и параметром. Это сохраняет читаемый синтаксис, не меняет состояние класса и позволяет переиспользовать одинаковые схемы через кэш.

Что кандидаты часто упускают

  1. Вопрос: Чем отличается Класс[Тип] от экземпляр[ключ] с точки зрения протокола?

    Ответ: В первом случае Python обрабатывает подписывание класса: при отсутствии приоритетного __getitem__ метакласса вызывается __class_getitem__ самого класса. Во втором случае подписывается экземпляр, поэтому используется __getitem__ его типа.

    Эти операции могут иметь одинаковый внешний синтаксис, но представляют разные уровни объектной модели. Метод __class_getitem__ не является заменой __getitem__ для экземпляров.

  2. Вопрос: Что произойдёт, если одновременно определить __class_getitem__ в классе и __getitem__ в его метаклассе?

    Ответ: При подписывании класса приоритет получит __getitem__ метакласса. Подписывание класса в таком случае обрабатывается как операция над объектом-классом, а класс является экземпляром своего метакласса.

    Поэтому добавление __class_getitem__ в класс не гарантирует, что именно он будет вызван. Это особенно важно при использовании стороннего метакласса: его реализация может изменить или перехватить поведение подписывания.

  3. Вопрос: Может ли __class_getitem__ возвращать сам класс без создания отдельного параметризованного объекта?

    Ответ: Технически может, поскольку протокол не навязывает конкретный тип результата. Однако тогда Класс[int] и Класс[str] могут стать неразличимыми, а информация о параметре будет потеряна.

    Такой результат допустим только если параметр используется исключительно как маркер или побочный механизм. Для обобщённых типов, схем сериализации и построителей запросов обычно нужен отдельный объект, сохраняющий исходный класс и переданные параметры.