Классы

Что class делает на самом деле, чем __new__ отличается от __init__, и почему super() — это не «вызвать родителя». Последнее ломает больше кода, чем любая другая ошибка в этом уроке.

L1 · 9 минL2 · 11 мин📱 телефонсверено · CPython 3.11, linux x86-64

1Предскажи

Фрагмент A
class B:
    def __new__(cls): return 42
    def __init__(self): print("init")

print(B())
42
Строчка init не напечаталась. Конструктор вернул число.
Фрагмент B
class X: pass
class Y(X): pass
class Z(X): pass
class W(Y, Z): pass
print([c.__name__ for c in W.__mro__])

class Bad(X, Y): pass
['W', 'Y', 'Z', 'X', 'object']
TypeError: Cannot create a consistent method resolution
order (MRO) for bases X, Y
Ошибка на объявлении класса, ещё до всякого вызова.
Фрагмент C
class Base:
    def setup(self): print("Base")

class Log(Base):
    def setup(self): print("Log"); super().setup()

class Cache(Base):
    def setup(self): print("Cache")      # super() забыли

class Svc(Log, Cache):
    def setup(self): print("Svc"); super().setup()

Svc().setup()
Svc
Log
Cache
Где Base? Он в MRO, у него есть setup, и он не выполнился.
Фрагмент D
class Registry:
    items = []                # атрибут класса

a = Registry(); b = Registry()
a.items.append(1)
print(b.items)
[1]
Два разных объекта, один список.

2Механизм

class — исполняемая инструкция, как и def. Тело класса выполняется как обычный блок кода в собственном пространстве имён; получившийся словарь имён передаётся конструктору типа, и тот создаёт объект-класс.

Создание экземпляра — два шага. __new__ создаёт объект и возвращает его; __init__ получает уже созданный и настраивает.

Порядок поиска в иерархии задан линеаризацией MRO, вычисляемой один раз при создании класса.

Следствие 1 · тело класса — это код, а не объявление

Фрагмент D объясняется прямо отсюда плюс уроком 01. Строка items = [] исполняется один раз, при создании класса, и создаёт один список, который становится атрибутом класса. Экземпляры своего списка не получают — они видят классовый по шагу 3 процедуры из урока 04.

Это та же ошибка, что общий изменяемый аргумент по умолчанию, в другой одежде: и там, и там выражение вычисляется однажды, в момент исполнения объявления. Одно понимание закрывает оба случая, и запоминать их по отдельности не нужно.

Из того же следует, что в теле класса можно писать любой код — циклы, условия, вычисления, — и что декоратор класса получает уже готовый объект.

Следствие 2 · __new__ создаёт, __init__ настраивает

Вызов B() — это не одна операция. Тип вызывает __new__, получает объект, и только если результат является экземпляром этого класса, вызывает на нём __init__.

Фрагмент A: __new__ вернул число, проверка не прошла, __init__ не вызвался. Никакой ошибки — язык считает это законным.

Отсюда выводится, когда какой метод нужен. __init__ — почти всегда: объект уже есть, надо заполнить поля. __new__ — только когда объект нельзя настроить после создания: неизменяемые типы (у str и tuple просто нет момента «после»), кэширование экземпляров, подмена возвращаемого класса. И следствие, объясняющее частую ошибку: __init__ не может ничего вернуть — если вернёт не None, будет TypeError.

Следствие 3 · MRO — линейное продолжение частичного порядка

Наследование задаёт на классах отношение «предок — потомок». При множественном наследовании это отношение частичное: про Y и Z нельзя сказать, кто из них раньше. C3-линеаризация строит из него линейный порядок с тремя свойствами: каждый класс ровно один раз, потомок строго раньше любого предка, и порядок базовых классов из объявления сохраняется.

Фрагмент B: для W(Y, Z) получается W, Y, Z, X, object — X обязан идти после обоих потомков. А для Bad(X, Y) порядка не существует: объявление требует X раньше Y, а наследование требует Y раньше X. Противоречие, и Python сообщает о нём при создании класса, а не при вызове метода.

Это редкий для динамического языка случай, когда ошибка ловится максимально рано — и он возможен именно потому, что линеаризация вычисляется один раз, а не при каждом обращении.

Следствие 4 · super() — это не родитель

Вот определение, которое стоит держать в голове дословно: super() передаёт управление следующему классу в MRO того объекта, на котором идёт вызов. Не родителю класса, где написана строчка. Не первому базовому. Следующему в линеаризации конкретного экземпляра.

Фрагмент C показывает, чем это отличается на практике. MRO у Svc — это Svc, Log, Cache, Base, object. Вызов идёт: Svc.setup → super() → Log.setup → super() → Cache.setup. Обрати внимание: Cache вообще не предок Log, они соседи по иерархии. И Log о существовании Cache ничего не знает — он просто передал управление дальше по цепочке.

А Cache.setup не вызвал super() — и цепочка оборвалась. Base.setup не выполнился, хотя стоит в MRO следующим. Ошибки нет, исключения нет, просто часть логики молча не отработала.

Отсюда правило кооперативного наследования, которое иначе выглядит суеверием: если метод переопределён в классе, участвующем во множественном наследовании, он обязан вызвать super() — даже если «родителя» у него в интуитивном смысле нет. Один пропуск в середине цепочки выключает всё, что стоит после.

3Границы модели

Где сказанное перестаёт держать

super() без аргументов работает только внутри тела класса. Компилятор подставляет туда скрытую ссылку на класс — отсюда переменная __class__ в замыкании метода. Вне тела класса нужна полная форма super(Cls, obj), и в динамически созданных методах это регулярно всплывает.

Кооперативность требует совместимых сигнатур. Если классы в цепочке принимают разные наборы аргументов, super() передавать их некому — отсюда соглашение принимать **kwargs и прокидывать дальше. Это дисциплина, а не механизм: язык её не проверяет.

Создание класса можно перехватить. Метакласс, __init_subclass__ и __set_name__ вклиниваются в момент, описанный в следствии 1 — этому посвящён урок 07. Пока достаточно знать, что точка расширения там есть.

4Корень

Корень тот же R2: всё резолвится в рантайме. Класс — обычный объект, созданный исполнением кода, его словарь можно менять после создания, а порядок поиска вычислен и хранится как данные, а не зашит в компилятор.

Эта конструкция появилась в 2001 году, когда Гвидо объединял типы и классы. До этого в Python было два вида классов с разными правилами, и MRO у старых вычислялся обходом в глубину слева направо — из-за чего в ромбе общий предок посещался раньше, чем второй потомок, и кооперативное наследование было невозможно. C3 взяли из Dylan; вместе с ней появились дескрипторы, property и super() — весь механизм из урока 04 и этого урока родился одним решением.

5Аналогия

Для математика формулировка точная: MRO — это топологическая сортировка ориентированного ациклического графа наследования, с дополнительным требованием сохранить локальный порядок базовых классов в каждом объявлении. C3 — конкретный алгоритм слияния, дающий такую сортировку, если она существует, и сообщающий об отказе, если нет.

Отсюда сразу понятно, почему Bad(X, Y) не создаётся: добавленные объявлением рёбра образуют цикл, а у графа с циклом топологической сортировки не бывает. И понятно, почему ошибка возникает при объявлении: сортировка считается один раз, тогда же.

6Вопросы пытливого ума

Вопросы, которые возникают сами, если читать внимательно. Ответ — под вопросом.

Зачем __new__, если есть __init__?

Затем, что __init__ получает уже созданный объект и не может решить, каким он будет.

Разделение простое: __new__ — конструктор, он объект создаёт и возвращает; __init__ — инициализатор, он полученный объект настраивает и обязан вернуть None.

Пока класс изменяемый и обычный, разница незаметна. Она становится принципиальной в двух случаях.

Неизменяемые типы. У tuple, str, int, frozenset значение задаётся при создании и больше не меняется — настраивать в __init__ нечего:

class Point(tuple):
    def __new__(cls, x, y):
        return super().__new__(cls, (x, y))     # единственное место, где можно задать содержимое

p = Point(1, 2)
print(p, p[0])      # → (1, 2) 1

Возврат чужого объекта. __new__ может вернуть не новый экземпляр — на этом стоят синглтоны, кеши объектов и фабрики, выбирающие подкласс по данным. Тонкость: если __new__ вернул объект не этого класса, __init__ вызван не будет вовсе.

Практически: в прикладном коде __new__ нужен редко, и почти всегда его хотят зря — для «синглтона» обычно достаточно модульной переменной, для фабрики — classmethod с понятным именем. Но при наследовании от неизменяемого встроенного типа обойти его невозможно.

Как super() без аргументов узнаёт, в каком он классе?

Компилятор дописывает ему подсказку — это не рантайм-магия, а решение фазы компиляции.

Если в теле метода встречается super, компилятор добавляет в функцию скрытую свободную переменную __class__, указывающую на класс, в котором метод определён. Видно прямо:

class A:
    def m(self): return super()

print(A.m.__code__.co_freevars)     # → ('__class__',)

class B:
    def m(self): return 1

print(B.m.__code__.co_freevars)     # → ()   подсказки нет

Дальше super() берёт __class__ и первый позиционный аргумент (то есть self) и превращается в super(__class__, self).

Отсюда все ограничения, которые кажутся произвольными. Вне тела класса super() без аргументов не работает — подсказки неоткуда взять. В staticmethod не работает — нет первого аргумента. Во вложенной функции внутри метода — работает, потому что ячейка захватывается замыканием. А если метод приделали к классу снаружи после его создания, придётся писать полную форму.

И главное, что стоит унести: super() идёт не к родителю, а к следующему классу в MRO инстанса. Для одиночного наследования это одно и то же, для множественного — нет, и именно поэтому кооперативное наследование работает только когда его соблюдают все участники цепочки.

Множественное наследование — почему не запретили, как в Java?

Потому что запрет в Java породил интерфейсы, а потом дефолтные методы в интерфейсах — то есть множественное наследование, вошедшее через другую дверь.

Проблема, из-за которой его боятся, — «ромб»: класс наследует от двух, у которых общий предок, и неясно, чей метод вызывать. Python решает её алгоритмом C3-линеаризации: строит один линейный порядок, в котором каждый класс встречается ровно раз, потомок всегда раньше предка, а порядок базовых классов сохраняется. Если такого порядка не существует, класс просто не создаётся:

class X: pass
class Y(X): pass

class Ok(Y, X): pass       # потомок раньше предка — порядок существует
print([c.__name__ for c in Ok.__mro__])
# → ['Ok', 'Y', 'X', 'object']

class Bad(X, Y): pass      # предок раньше потомка — противоречие
# → TypeError: Cannot create a consistent method resolution order (MRO)

Ошибка на этапе создания класса, а не при вызове метода — это и есть та гарантия, ради которой алгоритм выбран.

Практически множественное наследование в Python живёт в одной форме — mixin: маленький класс без своего состояния, добавляющий поведение. socketserver.ThreadingMixIn, collections.abc, половина Django. Это работает и читается.

Что не работает: наследование от двух полноценных классов со своим состоянием и своими __init__. Здесь super() требует, чтобы все участники цепочки вызывали super().__init__() и принимали **kwargs, и стоит одному нарушить — цепочка рвётся молча. Правило: если оба родителя хранят данные, это композиция, а не наследование.

Почему isinstance лучше, чем type(x) == C?

Потому что второе ломается на подклассах — и на классах, которые притворяются.

type(x) == C требует точного совпадения. Подкласс, который везде обязан работать как родитель, проваливает проверку — это прямое нарушение принципа подстановки:

class Money(int): pass
m = Money(5)
print(type(m) == int)      # → False
print(isinstance(m, int))  # → True

Второе отличие тоньше: isinstance проходит через __instancecheck__ метакласса, и это точка расширения. На ней стоят ABC из урока 11 — isinstance(x, collections.abc.Iterable) работает для любого класса с __iter__, вообще без наследования. И на ней же стоят typing.Protocol с runtime_checkable.

Где type(x) is C всё-таки уместен: когда точность и есть цель — сериализатор, который обязан различать bool и int (а bool наследник int, и isinstance(True, int) истинно), диспетчеризация по конкретному типу, отладочная проверка. В таких случаях пишут is, а не ==: типы — синглтоны.

И самый частый правильный ответ — не проверять тип вообще. Урок 11: если нужно «это последовательность», спрашивать надо про протокол, а не про класс.

Итог. class — исполняемая инструкция: тело выполняется как код, собранный словарь имён передаётся конструктору типа. Отсюда общий изменяемый атрибут класса — тот же механизм, что и общий аргумент по умолчанию. Создание экземпляра идёт в два шага: __new__ создаёт и возвращает, __init__ настраивает — и вызывается только если __new__ вернул экземпляр этого класса. Порядок поиска задан C3-линеаризацией, то есть топологической сортировкой графа наследования: она вычисляется один раз при объявлении, и если совместимого порядка нет, класс не создаётся вовсе. И главное: super() передаёт управление следующему классу в MRO конкретного объекта, а не родителю — поэтому он связывает классы, ничего не знающие друг о друге, и поэтому один пропущенный super() в середине цепочки молча выключает всё, что стоит после.
Дальше — по желанию
контрфактуалJava, C++, Ruby — три ответа на множественное наследованиеЗапрет, ручное управление или примеси; линеаризация только у Python
Контрфактуал · те же вопросы у других

Java и C# запретили множественное наследование реализации и оставили интерфейсы. Проблемы MRO не существует, потому что не существует ситуации, её порождающей. Цена — примеси приходится собирать композицией и делегированием вручную.

C++ разрешил множественное наследование и не стал линеаризовать: общий предок дублируется, если не объявить его виртуальным, и порядок вызовов программист задаёт сам. Больше контроля, больше способов ошибиться.

Ruby выбрал третий путь: одиночное наследование плюс модули-примеси, которые вставляются в цепочку поиска. Это фактически линеаризация, только собираемая явными include вместо вычисления по графу.

Три ответа на один вопрос «что делать, когда поведение приходит из нескольких мест». Python единственный вычисляет ответ алгоритмом и отказывается создавать класс, если ответа нет.

🐛 багПримесь без super() рвёт цепочкуДобавили класс — часть инициализации молча перестала выполняться, падает позже и не там
🐛 Баг, который проходит ревью

Добавили примесь в иерархию — и часть инициализации перестала выполняться. Молча.

class Service:
    def __init__(self, **kw):
        self.conn = connect()
        super().__init__(**kw)

class WithMetrics(Service):
    def __init__(self, **kw):
        self.metrics = Metrics()
        super().__init__(**kw)

class WithCache(Service):
    def __init__(self, **kw):
        self.cache = {}            # super() забыли

class Api(WithMetrics, WithCache):
    pass

Ревьюер смотрит на WithCache отдельно: класс наследует Service, заводит свой кэш, всё логично. В изоляции он даже работает — если создать WithCache() напрямую, Service.__init__ вызовется, потому что WithCache следующий в его собственном MRO... нет, не вызовется. И это первое, что стоит проверить самому.

А в Api цепочка обрывается на WithCache, и self.conn не создаётся вовсе. Падение произойдёт позже и в другом месте — при первом обращении к соединению, с AttributeError, который выглядит как «забыли инициализировать» и уводит расследование не туда.

Почему ревью пропускает: диффом видно один добавленный класс, а сломалась цепочка, которой в диффе нет. Проверять надо не класс, а MRO результата:

print([c.__name__ for c in Api.__mro__])
# → ['Api', 'WithMetrics', 'WithCache', 'Service', 'object']

Правило, выводимое из следствия 4: в иерархии с множественным наследованием каждый переопределённый метод обязан звать super(), даже когда кажется, что звать некого. И **kwargs прокидывать дальше — иначе цепочка оборвётся на несовпадении сигнатур.

⚡ приёмСпрашивать MRO, а не читать иерархию__mro__ и __qualname__ дают точный ответ за секунду вместо часов по исходникам
⚡ Приём: проверять MRO, а не читать иерархию глазами

Разбирать множественное наследование по исходникам — часы. Спросить у языка — секунда.

[c.__name__ for c in SomeClass.__mro__]
SomeClass.method.__qualname__        # чей метод реально победил
import inspect
inspect.getmro(SomeClass)
inspect.getsource(SomeClass.method)  # и его исходник

Линеаризация — это данные, лежащие в объекте класса, а не результат вывода по коду. Значит любой вопрос «откуда пришёл этот метод» и «в каком порядке пойдут вызовы» имеет точный ответ, который не надо реконструировать.

Практический порядок при разборе чужой иерархии: сначала __mro__ — увидеть реальную цепочку; потом __qualname__ нужного метода — понять, чья реализация активна; потом getsource — прочитать именно её, а не ту, что нашлась поиском по имени. Три строки вместо чтения пяти файлов, и ответ гарантированно верный, потому что это то же самое, чем пользуется интерпретатор.

Тот же приём закрывает и обратный вопрос — «почему мой метод не вызывается»: если класс стоит в MRO после того, кто оборвал цепочку, ответ виден сразу.

💻 терминалПроверь в REPLЧетыре фрагмента плюс несвязанные классы в цепочке super и наследование от dict

Проверь

Прогони четыре фрагмента, сверяясь с предсказанием. Затем:

class A:
    def __init__(self): print("A"); super().__init__()
class B:
    def __init__(self): print("B"); super().__init__()
class C(A, B): pass
C()
# Почему B напечатается, хотя A и B не связаны наследованием?

class D(dict):
    def __init__(self): self["x"] = 1
D()
# Работает без вызова super().__init__(). Почему?
# А для класса, наследующего tuple, так не выйдет — почему?

class E:
    x = [1]
e = E(); e.x = e.x + [2]
E.x
# А если написать e.x += [2]? Разный результат — объясни через урок 01.

# Без кода: почему ошибка MRO возникает при объявлении класса,
# а ошибка «нет такого атрибута» — только при обращении?

Первый вопрос — ключевой: он показывает, что super() связывает классы, которые ничего друг о друге не знают, и что кооперативность работает именно за счёт линеаризации.

исходникиenum и socketserverПорядок членов перечисления держится на упорядоченности словаря из L05

Где это живёт в реальном коде

Lib/enum.py, метод EnumType.__new__. Здесь следствие 1 используется на полную: тело класса перечисления исполняется как обычный код, все имена собираются в словарь, а затем конструктор типа превращает их в объекты-члены перечисления.

Заметь, почему порядок членов сохраняется: пространство имён тела класса — словарь, а словари упорядочены по вставке с 3.7 (урок 05). До этой гарантии enum приходилось подкладывать специальный тип словаря через __prepare__. Один урок опирается на другой буквально в коде стандартной библиотеки.

Второе место — Lib/socketserver.py. Там классы вроде ThreadingTCPServer собираются множественным наследованием из примесей, и видно живой кооперативный super() — ровно та конструкция, которую ломает фрагмент C.

L2Как класс создаётся и где лежит линеаризацияПолная последовательность создания класса и где живёт __class__ для super()

Из L1 ты знаешь, что тело класса исполняется, а MRO вычисляется один раз. Здесь — последовательность и стоимость.

Полный порядок создания класса: компилятор превращает тело в отдельный объект кода; при исполнении class этот код выполняется в свежем словаре; определяется метакласс (из аргумента metaclass или как наиболее производный среди метаклассов баз); вызывается его __prepare__, отдающий словарь для тела; выполняется тело; вызывается метакласс с именем, кортежем баз и заполненным словарём; внутри вычисляется MRO, выставляются слоты типа, вызываются __set_name__ у дескрипторов и __init_subclass__ у ближайшего предка.

Результат линеаризации хранится в самом объекте типа и доступен как __mro__. Он неизменяем: присвоить туда свой кортеж нельзя. Но пересчитывается он при изменении __bases__ — и вместе с этим поднимается версия типа, обесценивая инлайн-кеши из урока 04. Отсюда практический эффект: динамическая правка иерархии в горячем коде стоит не только самой правки, но и потери всей специализации.

print(W.__mro__)
# → (<class 'W'>, <class 'Y'>, <class 'Z'>, <class 'X'>, <class 'object'>)
W.__mro__ = (...)
# → AttributeError: readonly attribute

Про super() без аргументов: компилятор, встретив его в теле метода, добавляет в замыкание ячейку __class__. Убедиться легко — у такого метода появляется __closure__, которого у обычного нет. Отсюда и правило из раздела 3: вне тела класса ячейки нет, и форма без аргументов не работает.

L3Где это в исходниках CPythontypeobject.c: mro_implementation, type_new, super_getattro

Тег v3.11.15.

  • Objects/typeobject.c, mro_implementation и pmerge — C3 целиком. Слияние занимает страницу и читается легко, если помнить формулировку про топологическую сортировку.
  • Там же type_new — вся последовательность создания класса из раздела L2, шаг за шагом.
  • Objects/typeobject.c, super_getattro — как super() ищет следующий класс: он берёт MRO типа объекта и идёт от позиции своего класса, а не от начала.
  • The Python 2.3 Method Resolution Order — статья, которой ввели C3; лучший разбор алгоритма с примерами противоречий.
связиКуда это ведётL01, L04, L05, L07 · контраст с интерфейсами и примесями

Связи

корень R2 · Атрибут резолвится в рантайме, неймспейс — dict
← основа L04 · obj.x насквозь — MRO это шаг 1 и 3 той процедуры
← основа L01 · Имя, объект, ссылка — общий изменяемый атрибут класса из фрагмента D
← основа L05 · dict и хешируемость — тело класса это словарь, и его упорядоченность видна в enum
↔ контраст Интерфейсы в Java: проблема MRO устранена запретом ситуации, а не алгоритмом
↔ контраст Модули-примеси в Ruby: та же линеаризация, но собираемая вручную
→ дальше L07 · Метаклассы — точка расширения из следствия 1 целиком
чего здесь нет dataclass и attrs как генераторы классов — это применение метаклассов и декораторов, урок 07
источникиЧто почитатьMRO 2.3 🟥 · создание класса 🟧 · super 🟦

Что почитать