super() без аргументов опирается на сгенерированную компилятором ячейку замыкания с именем __class__, которая создаётся неявно для любого метода, определённого на лексическом уровне внутри тела класса Python. Когда компилятор обрабатывает определение класса, он создаёт ячейку переменной __class__ в замыкании метода, указывающую на объект класса, который в данный момент определяется. Когда super() вызывается без аргументов, реализация на C проверяет текущий контекст вызова, находит эту ячейку __class__ и использует её как первый аргумент (тип). Затем используется первый позиционный аргумент метода (обычно self) в качестве экземпляра. Этот механизм связывает ссылку на класс во время определения, а не во время вызова, устраняя необходимость жестко кодировать имена классов, при этом гарантируя, что каждый метод в цепочке наследования ссылается на своё конкретное положение в MRO (Порядок разрешения методов).
class Base: def method(self): return "Base" class Middle(Base): def method(self): # __class__ связывается с Middle здесь return f"Middle -> {super().method()}" class Derived(Middle): def method(self): # __class__ связывается с Derived здесь return f"Derived -> {super().method()}"
Мы поддерживали библиотеку квантовой торговли с глубокой иерархией моделей ценообразования. BaseModel предоставил метод calculate_risk(), EquityModel переопределил его, чтобы добавить логику, специфичную для акций, а AmericanOptionModel ещё более специализировано его изменил. Во время крупной переработки, чтобы переименовать EquityModel в VanillaEquityModel, мы обнаружили десятки устаревших вызовов super(EquityModel, self) в миксинах, которые были скопированы и вставлены. Эти устаревшие ссылки вызывали TypeError или тихие логические ошибки, когда вызывался неверный родительский метод, что нарушало вычисления рисков в производственной среде.
Решение 1: Глобальный поиск и замена. Мы рассматривали возможность использования автоматизированных инструментов для поиска и замены всех жестко закодированных имен классов в вызовах super() по всей кодовой базе в 200,000 строк. Плюсы: Это не требует архитектурных изменений и работает с синтаксисом старого Python 2. Минусы: Это хрупко и неполно; это пропускает динамически сгенерированные классы, присваивание динамических методов на основе строк и ссылки в сторонних расширениях. Это также нарушает принцип DRY, так как имя класса повторяется в каждом методе.
Решение 2: Универсальное использование super() без аргументов. Мы мигрировали всю кодовую базу на использование super() без аргументов. Плюсы: Это делает переименование классов совершенно безопасным, устраняет основной источник человеческих ошибок во время переработки и значительно улучшает читаемость, убирая лишний шум. Это корректно обрабатывает сложные паттерны многократного наследования. Минусы: Это требует Python 3.6+ (с которым мы работали), и разработчики, незнакомые с неявным механизмом замыкания, поначалу находили это запутанным. Также это не может быть использовано в функциях, динамически прикрепляемых к классам после их определения.
Решение 3: Инъекция ссылок на классы через метакласс. Мы ненадолго рассматривали возможность использования метакласса для инъекции атрибута _defining_class в каждый метод. Плюсы: Это делает механизм явным и доступным для проверки. Минусы: Это добавляет значительную сложность и накладные расходы, конфликтует со стандартной оптимизацией CPython и изобретает функцию, уже предоставляемую компилятором языка.
Мы выбрали Решение 2. Миграция была завершена за один спринт. В результате мы сократили время, затрачиваемое на последующие задачи по переработке, связанные с переименованием классов, на 40%, и устранили целый класс ошибок, связанных с устаревшими ссылками на super() в нашем CI-пipeline.
Как super() физически находит ячейку __class__, когда вызывается без аргументов?
Реализация super() в CPython (в Objects/typeobject.c) использует PyEval_GetLocals(), чтобы проверить локальные переменные и замыкания в текущем контексте вызова. Она специально ищет свободную переменную (ячейку), названную __class__. Эта ячейка создаётся компилятором только тогда, когда функция определена на лексическом уровне внутри тела класса (обозначается флагом CO_OPTIMIZED и областью класса). Если ячейка найдена, super() извлекает объект класса; если нет, он возбуждает RuntimeError: super(): ячейка __class__ не найдена. Безаргументная форма фактически преобразуется компилятором в super(__class__, self), где __class__ — это закрытая переменная.
Что происходит, если вы попытаетесь использовать super() без аргументов внутри функции, которая присваивается атрибуту класса после создания класса?
Если вы определяете функцию вне тела класса, а затем присваиваете её как метод (например, MyClass.method = some_function), вызов super() внутри этой функции вызовет RuntimeError. Это происходит потому, что компилятор создаёт ячейку __class__ только для кодовых объектов, скомпилированных как часть класса. Без этой ячейки super() не может определить, какой класс в иерархии является "текущим" классом, так как он не может отличить область определения функции от класса, к которому она была позже прикреплена.
Почему super() без аргументов не вызывает бесконечную рекурсию, когда метод подкласса вызывает super(), и родительский метод также вызывает super()?
Это работает потому, что __class__ ссылается на класс, где метод определён, а не на классовый тип экземпляра (type(self)). Когда вызывается Derived.method(), он находит, что __class__ — это Derived, и делегирует вызов следующему классу в Derived.__mro__ (например, Middle). Когда достигается Middle.method(), и он вызывает super(), его собственная ячейка __class__ содержит Middle, поэтому он ищет следующий класс после Middle (например, Base). Каждый уровень иерархии использует свою собственную ссылку на класс времени определения, обеспечивая линейный подъем по MRO ровно один раз без зацикливания на подклассе.