1. Магия super() и замыкания: зачем Python скрывает ссылку на класс внутри методов
Магия super() и замыкания: зачем Python скрывает ссылку на класс внутри методов
Добро пожаловать на курс «Глубокое погружение в __classcell__ и метапрограммирование Python». Мы начинаем наше путешествие с темы, которая часто остается «под капотом» даже для опытных разработчиков. Вы наверняка тысячи раз писали super().__init__(), но задумывались ли вы, как именно эта функция понимает, кто является родителем текущего класса, если сам класс еще даже не до конца создан в момент написания кода?
Сегодня мы вскроем механизм, который делает возможным использование super() без аргументов в Python 3. Мы поговорим о __classcell__ — скрытом элементе, который связывает пространства имен, замыкания и метаклассы в единое целое.
Проблема курицы и яйца: как метод узнает о своем классе?
Чтобы понять суть __classcell__, давайте вернемся немного назад. В Python 2 вызов родительского метода выглядел так:
Вы должны были явно передать два аргумента: текущий класс и экземпляр (или класс, если метод классовый). Это было неудобно и нарушало принцип DRY (Don't Repeat Yourself). Если вы переименовывали класс, приходилось менять имя и внутри super.
В Python 3 мы пишем просто:
Это выглядит как магия. Откуда super() внутри метода save знает, что он находится именно внутри MyModel?
Первая мысль, которая приходит в голову новичкам: «Наверное, он берет self.__class__». Это фатальная ошибка.
Если бы super() использовал self.__class__, наследование сломалось бы мгновенно. Представьте ситуацию:
Parent.Child(Parent).c = Child().c.method(), который вызывает super().Если бы super смотрел на self.__class__, он бы увидел Child. MRO (Method Resolution Order — порядок разрешения методов) для Child указывает, что следующий класс — Parent. Метод Parent вызывается. Если внутри Parent тоже есть super(), и он снова посмотрит на self.__class__ (который все еще Child!), мы попадем в бесконечную рекурсию.
> Для корректной работы super() методу нужно знать не класс экземпляра (self), а класс, в котором этот метод определен.
Но здесь возникает парадокс: в момент, когда мы пишем тело метода def save(...), класс MyModel еще не существует. Он только создается.
Анатомия магии: Замыкания и __class__
Python решает эту проблему с помощью замыканий (closures). Когда компилятор видит вызов super() без аргументов или явное использование имени __class__ внутри метода, он делает этот метод замыканием.
Он добавляет в локальные переменные метода скрытую ссылку на переменную __class__. Эта переменная должна указывать на сам класс MyModel.
Давайте проверим это на практике:
Мы видим, что метод хранит ссылку на класс Inspector. Но как эта ссылка туда попала, если класс создается после выполнения тела класса?
Роль __classcell__
Здесь на сцену выходит __classcell__. Это механизм передачи ссылки на будущий класс в замыкания его методов.
Когда Python исполняет тело класса, он создает пространство имен (обычный словарь), куда складывает все определенные методы и атрибуты. Если компилятор заметил, что методы требуют ссылку на класс (используют super()), он добавляет в это пространство имен специальный ключ: __classcell__.
Как это работает пошагово:
__prepare__): Метакласс создает пустой словарь для пространства имен класса.class MyClass: .... Методы определяются, но пока они «пустые» в плане знания о своем классе.__classcell__: Если хоть один метод использовал super(), компилятор создает объект ячейки (cell object) и кладет его в словарь атрибутов под ключом __classcell__.type.__new__): Метакласс берет имя класса, базы и словарь атрибутов (включая __classcell__) и создает объект класса.type.__new__ берет только что созданный объект класса и «кладет» его внутрь той самой ячейки, на которую ссылается __classcell__.Подводные камни: Когда магия ломается
Как профессиональный разработчик, вы столкнетесь с __classcell__ лицом к лицу, когда начнете писать свои метаклассы или сложные декораторы классов. Это классическая ситуация «подводного камня».
Ошибка 1: Потеря ячейки в метаклассе
Представьте, что вы пишете метакласс для ORM, который фильтрует атрибуты класса. Вы решаете создать новый словарь атрибутов, исключив все «лишнее».
Результат: TypeError: __class__ set to <...> defining 'Broken' as <...> или RuntimeError при вызове метода.
Почему: type.__new__ требует, чтобы если методы ожидают __class__, то __classcell__ должен присутствовать в передаваемом словаре attrs. Если вы его выкинули, цепочка замыкания разрывается.
Решение: Всегда проверяйте наличие __classcell__ в исходных атрибутах и переносите его в новый словарь.
Ошибка 2: Конфликт при декорировании
Если вы вручную создаете класс через types.new_class или type(...) и пытаетесь подсунуть туда функции, которые уже были скомпилированы как методы другого класса, вы можете получить ошибку о несовпадении ячеек.
Практическое применение: Django и FastAPI
Зачем вам это знать, если вы используете фреймворки?
Django Models и Forms
Django интенсивно использует метаклассы (ModelBase, FormMetaclass). Когда вы пишете миксины для моделей или переопределяете поведение сохранения:
Если бы вы попытались реализовать свою систему плагинов для Django, динамически создавая классы моделей на лету (например, для CMS), и забыли бы пробросить __classcell__, ваши вызовы super().save() в сгенерированных моделях упали бы с крашем.
FastAPI и Pydantic
Pydantic (на котором стоит FastAPI) использует метаклассы для валидации полей. При создании динамических моделей через create_model Pydantic берет на себя сложную работу по управлению пространством имен.
Однако, если вы пишете сложный Depends класс, который наследуется и использует super(), и пытаетесь обернуть его декоратором, меняющим структуру класса, вы рискуете сломать механизм внедрения зависимостей, если декоратор не уважает протокол __classcell__.
Резюме
super() без аргументов — это синтаксический сахар, который опирается на переменную __class__ в замыкании метода.__classcell__ — это специальный атрибут, который появляется в пространстве имен класса во время его создания, если методы используют super().type.__new__ заполняет эту ячейку ссылкой на созданный класс, тем самым «замыкая» круг и давая методам доступ к классу, которому они принадлежат.__classcell__ из словаря атрибутов. Всегда передавайте его дальше в super().__new__.Понимание этого механизма отличает простого пользователя Python от инженера, способного отлаживать самые запутанные ошибки в библиотеках и фреймворках. В следующей статье мы углубимся в то, как __prepare__ позволяет нам менять саму природу словаря класса еще до того, как написана первая строчка кода.