Deep Engineering
Средний·Опубликовано·3.11 · 3.12 · 3.13 · 3.14·30 МИН

Классы и ООП: self — это аргумент, а атрибут класса — один на всех

Первые вопросы по ООП звучат просто: что такое self, чем атрибут класса отличается от атрибута экземпляра, зачем classmethod и staticmethod, что делает __init__. Прогон отвечает на каждый: a.hello() и Greeter.hello(a) — один и тот же вызов; список, объявленный в теле класса, — один на все экземпляры; __init__ получает уже созданный объект; а класс с __eq__, но без __hash__, перестаёт быть ключом словаря и элементом множества.

Полное техническое изложение

TL;DR

Класс в Python — объект, в котором лежат общие для всех экземпляров атрибуты и методы; экземпляр обычного класса — объект со своим словарём атрибутов. (Класс со __slots__ может обойтись без словаря экземпляра, но только при условиях, разобранных в конце урока.) Метод — это обычная функция в теле класса, а self — её обычный первый аргумент: a.hello() и Greeter.hello(a) — один и тот же вызов. Python просто подставляет экземпляр первым аргументом сам.

Отсюда главное следствие: для обычного атрибута чтение self.имя ищет сначала в экземпляре, потом в классе, а присваивание self.имя = … пишет в экземпляр. Поэтому c1.total = 5 класс не меняет, а c1.tags.append(…) — меняет: это чтение общего списка, объявленного в теле класса. Три экземпляра, добавившие по метке в такой список, видят все три метки, а в их собственных словарях tags нет.

Дальше — то, что отличает понимание от пересказа. a.hello — новый объект связанного метода при каждом обращении: тысяча обращений дали тысячу разных объектов. @classmethod получает класс, через который его вызвали, — поэтому фабрика Dog.create() возвращает Dog, а не родителя. __init__ — не конструктор: объект к его вызову уже создан __new__. А класс, который определил __eq__ и забыл __hash__, перестаёт быть ключом словаря и элементом множества: TypeError: unhashable type.

Порог входа
Перед уроком достаточно понимать
  • как объявить функцию и вызвать её с аргументами;
  • что такое список и словарь и что список можно изменить на месте;
  • что объявление класса — это код, который выполняется сверху вниз;
  • что такое множество (set) и что ключ словаря и элемент множества должны быть хешируемыми — то есть иметь хеш, число, по которому словарь раскладывает ключи; подробно — в уроке о встроенных коллекциях, он идёт по линии следующим;
  • что строка @имя над функцией — декоратор: он оборачивает функцию; здесь достаточно встроенных @classmethod, @staticmethod и @dataclass, подробно — в уроке о декораторах, позже по линии.
Заранее знать не нужно
  • self, связанный метод, атрибут класса и атрибут экземпляра, vars();
  • @classmethod, @staticmethod, super().__init__(), _x и __x;
  • __new__, __init__, __eq__, __hash__ и __slots__.

На чём проверено: весь вывод в уроке — прогоны bench/oop/basics.py и bench/oop/practice.py на CPython 3.11.15, 3.12.3, 3.13.7 и 3.14.7, linux/amd64; блоки в тексте — запись на 3.13.7, расхождения версий — в «Истории версий». Цитаты документации сверены 14.10.2026. На эту дату вышел Python 3.15 (9 октября 2026); урок на нём не запускался.

База: класс — это объект с общими атрибутами

Класс объявляют так. Полный файл — импорты не нужны; запуск: python3 greeter.py.

PYTHON
class Greeter:
    def __init__(self, name):
        self.name = name
 
    def hello(self):
        return f"привет, {self.name}"
 
a = Greeter("Аня")
print(a.hello())        # привет, Аня

Greeter("Аня") создаёт экземпляр и вызывает у него __init__, который кладёт в экземпляр атрибут name. Метод hello лежит в классе, один на все экземпляры, а данные — name — в каждом экземпляре свои.

Ответ уровня Junior на «что такое класс»: класс описывает данные и поведение; данные лежат в экземплярах, поведение — методы — в классе, общее для всех. self — ссылка на экземпляр, с которым работает метод. Это верно, но за этим ответом сразу спрашивают, откуда берётся self и что будет, если положить данные не в экземпляр, а в класс. Об этом — механизмы ниже.

Наследование, инкапсуляция и полиморфизм в Python тоже есть, но устроены проще, чем в языках, где их проходят первыми: наследник ищет атрибут у родителя, «приватность» — соглашение об именах, а полиморфизм — просто вызов метода с тем же именем у объекта другого класса.

Механизм 1: self — обычный первый аргумент

контракт языкаСвязь метода и функции задана моделью данных; вывод — прогон bench/oop/basics.py, раздел 1, одинаковый на CPython 3.11–3.14.

self не ключевое слово и не магия. Документация описывает вызов метода так:

When an instance method object is called, the underlying function (__func__) is called, inserting the class instance (__self__) in front of the argument list.

перевод

Когда вызывается объект метода экземпляра, вызывается лежащая под ним функция (__func__), и экземпляр класса (__self__) вставляется в начало списка аргументов.

— Модель данных — методы экземпляра

Прогон показывает это напрямую — вывод bench/oop/basics.py, раздел 1:

1) self — обычный первый аргумент
   a.hello()          -> привет, Аня
   Greeter.hello(a)   -> привет, Аня
   type(a.hello)      -> method
   a.hello is a.hello -> False
   a.hello == a.hello -> True
   a.hello.__self__ is a            -> True
   a.hello.__func__ is Greeter.hello -> True
   Greeter.hello()    -> TypeError: Greeter.hello() missing 1 required positional argument: 'self'
   a.broken()         -> TypeError: Greeter.broken() takes 0 positional arguments but 1 was given
   1000 обращений a.hello, разных объектов: 1000

Три вещи, которые отсюда стоит вынести.

  • a.hello — связанный метод: маленький объект, который помнит функцию (__func__) и экземпляр (__self__). Вызов связанного метода вызывает функцию, подставив экземпляр первым аргументом.
  • Он создаётся заново при каждом обращении: a.hello is a.hello — False, хотя они равны. Как именно функция в классе превращается в связанный метод, разобрано в уроке о дескрипторах; сколько это стоит и почему при вызове a.hello() объект обычно не создаётся вовсе — в статье о поиске атрибута.
  • Забытый self ломается при первом вызове через экземпляр, и сообщение об ошибке говорит ровно об этом: функции, объявленной без параметров, передали один аргумент — экземпляр.

Механизм 2: атрибут класса и атрибут экземпляра

контракт языкаПравило поиска и записи — справочник языка (определения классов); вывод — прогон bench/oop/basics.py, раздел 2, одинаковый на CPython 3.11–3.14.

Документация формулирует правило в одном абзаце:

Variables defined in the class definition are class attributes; they are shared by instances. […] Class attributes can be used as defaults for instance attributes, but using mutable values there can lead to unexpected results.

перевод

Переменные, определённые в теле класса, — атрибуты класса; они общие для экземпляров. […] Атрибуты класса можно использовать как значения по умолчанию для атрибутов экземпляра, но изменяемые значения там могут привести к неожиданным результатам.

— Справочник языка — определения классов

«Неожиданные результаты» выглядят так — вывод bench/oop/basics.py, раздел 2:

2) атрибут класса против атрибута экземпляра
   до присваивания: vars(c1) = {} | c1.total = 0
   после c1.total = 5: vars(c1) = {'total': 5}
   c1.total = 5 | c2.total = 0 | Counter.total = 0
   три экземпляра добавили по метке в tags класса:
   c1.tags = ['t0', 't1', 't2'] | c2.tags = ['t0', 't1', 't2']
   c1.tags is c2.tags -> True | vars(c2) = {}
   список в __init__: f1.tags = ['t0'] | f2.tags = []

Всё объясняется одним правилом с двумя половинами.

  • Чтение c1.имя ищет сначала в словаре экземпляра (vars(c1)), потом в классе. Пока у экземпляра своего атрибута нет, все экземпляры читают одно классовое значение.
  • Присваивание c1.имя = … пишет в словарь экземпляра. После c1.total = 5 у c1 свой total, и он затеняет классовый; класс и остальные экземпляры не изменились.

Правило верно для обычных атрибутов — а таких в коде подавляющее большинство. Исключение одно, и о нём спрашивают на следующем шаге: если в классе под этим именем лежит дескриптор данных — например, property, — то и чтение, и запись идут через него, а не через словарь экземпляра. Это разобрано в уроке о дескрипторах. Отдельно стоит __slots__: имена из него документация реализует теми же дескрипторами, а словарь экземпляра он может убрать — но не всегда; условия — в разделе «Глубже: __slots__ убирает словарь не всегда» ниже.

Ловушка в том, что c1.tags.append(…) — не присваивание. Это чтение c1.tags, которое находит список в классе, и вызов метода у этого списка. Список один, поэтому меняется у всех. Лечится тем, что изменяемое значение создаётся в __init__: self.tags = [] — тогда у каждого экземпляра свой. Тот же механизм — значение вычислено один раз и общее для всех — стоит за изменяемым аргументом по умолчанию у функций, и в уроке о нём разобрано, как эту ошибку ловят датаклассы.

Механизм 3: три вида методов

контракт языкаЧто получает метод первым аргументом, задано моделью данных; вывод — прогон bench/oop/basics.py, раздел 3, одинаковый на CPython 3.11–3.14.

Методы различаются тем, что получают первым аргументом:

методпервый аргументдля чего
обычныйэкземпляр (self)работа с данными экземпляра
@classmethodкласс (cls), через который вызвалиальтернативные конструкторы, работа с атрибутами класса
@staticmethodничего лишнегофункция, которой удобно жить в пространстве имён класса

Вывод bench/oop/basics.py, раздел 3:

3) три вида методов: что приходит первым аргументом
   describe получил экземпляр Dog
   who получил класс Dog | who получил класс Dog
   helper получил только x = 1 | helper получил только x = 2
   Animal.create() -> Animal
   Dog.create()    -> Dog
   Animal.describe() без экземпляра -> TypeError: Animal.describe() missing 1 required positional argument: 'self'
   MyDict.fromkeys('ab')         -> MyDict
   MyDateTime.fromtimestamp(0)   -> MyDateTime

Главное здесь — строки про create. Метод create объявлен в Animal и делает return cls(). Вызванный через Dog, он получает cls = Dog и создаёт собаку, хотя написан в родителе. Так же устроены фабрики стандартной библиотеки: последние две строки — dict.fromkeys и datetime.fromtimestamp, вызванные через наследников, вернули наследников. Если бы фабрика писала Animal() явно, наследник получал бы из неё объект родителя.

@staticmethod не получает ни экземпляра, ни класса. Вызывать его можно и через класс, и через экземпляр — ведёт он себя одинаково.

Механизм 4: наследование и super().__init__()

контракт языкаТребование вызвать __init__ базового класса — модель данных; вывод — прогон bench/oop/basics.py, раздел 4, одинаковый на CPython 3.11–3.14.

Наследник ищет у родителя всё, чего нет у него самого. Но если наследник объявил свой __init__, родительский сам не вызовется:

If a base class has an __init__() method, the derived class's __init__() method, if any, must explicitly call it to ensure proper initialization of the base class part of the instance.

перевод

Если у базового класса есть метод __init__(), то __init__() производного класса, если он есть, обязан явно вызвать его, чтобы правильно инициализировать базовую часть экземпляра.

— Модель данных — __init__

Вывод bench/oop/basics.py, раздел 4:

4) наследование и super().__init__()
   без super(): объект создан, vars = {'rate': 0.05}
   s1.balance -> AttributeError: 'SavingsForgot' object has no attribute 'balance'
   с super():   vars = {'owner': 'Ира', 'balance': 0, 'rate': 0.05}
   isinstance(s2, Account) -> True
   type(s2) is Account     -> False
   Savings.__mro__ -> ['Savings', 'Account', 'object']

Без super().__init__() объект создаётся без ошибки, и AttributeError появляется позже — при первом чтении атрибута родителя, далеко от места, где вызов забыли. Строки про isinstance и type(s2) is Account — разница между проверкой на принадлежность и сравнением типа: экземпляр наследника является экземпляром родителя, но его тип — не родитель. Поэтому проверяют через isinstance, если наследники должны проходить проверку.

Порядок, в котором Python ищет атрибут по цепочке классов, лежит в __mro__. Строит его алгоритм C3; при одиночном наследовании результат очевиден — класс, родитель, object, — а при множественном уже нет: это урок о MRO.

Механизм 5: _x и __x — соглашение, а не защита

контракт языкаПравило искажения имён — справочник языка; вывод — прогон bench/oop/basics.py, раздел 5, одинаковый на CPython 3.11–3.14.

Модификаторов доступа в Python нет. Есть два соглашения об именах:

  • _x — «это внутреннее, снаружи не трогайте». Интерпретатор с этим ничего не делает: атрибут доступен как любой другой.
  • __x внутри класса переименовывается (кроме имён, которые и заканчиваются двумя подчёркиваниями, — это магические методы вроде __init__) — the identifier __spam occurring in a class named Foo, _Foo or __Foo is transformed to _Foo__spam (идентификатор __spam, встретившийся в классе с именем Foo, _Foo или __Foo, превращается в _Foo__spam).

Вывод bench/oop/basics.py, раздел 5:

5) _x и __x: соглашение и переименование
   vars(w) = {'_cash': 10, '_Wallet__pin': 1234}
   w._cash -> 10
   w.__pin -> AttributeError: 'Wallet' object has no attribute '__pin'
   w._Wallet__pin -> 1234
   наследник с тем же __pin: vars = {'_cash': 10, '_Wallet__pin': 1234, '_StrictWallet__pin': 0}
   sw.pin_ok(1234) -> True

Переименование не прячет атрибут — w._Wallet__pin читается свободно. Оно защищает от другого: от случайного совпадения имён в наследнике. Наследник объявил свой __pin, и он лёг рядом с родительским, а не поверх: метод родителя pin_ok по-прежнему видит свой _Wallet__pin. Это и есть ответ на «есть ли в Python private»: нет, есть защита от коллизий.

Глубже: __init__ — не конструктор, а __eq__ отключает хеш

контракт языкаПорядок __new__ и __init__ и правило __hash__ = None — модель данных; вывод — прогон bench/oop/basics.py, раздел 6. Текст исключения в 3.14 изменился — версии в истории ниже.

Два магических метода, вокруг которых на собеседовании чаще всего ошибаются.

__init__ получает уже созданный объект. Документация: called after the instance has been created (by __new__()), but before it is returned to the caller (вызывается после того, как экземпляр создан (методом __new__()), но до того, как он возвращён вызывающему). Конструктор в привычном смысле — пара: __new__ создаёт объект, __init__ заполняет его. __new__ переопределяют редко: документация называет два случая — наследники неизменяемых типов вроде int, str и tuple и метаклассы.

__eq__ без __hash__ делает объект нехешируемым.

A class that overrides __eq__() and does not define __hash__() will have its __hash__() implicitly set to None.

перевод

У класса, который переопределяет __eq__() и не определяет __hash__(), __hash__() неявно становится None.

— Модель данных — __hash__

Вывод bench/oop/basics.py, раздел 6:

6) магические методы: __new__, __init__, __eq__, __hash__
   __new__ создаёт объект
   __init__ заполняет уже созданный объект, x = 1
   Plain(1, 2) == Plain(1, 2) -> False
   Point(1, 2) == Point(1, 2) -> True
   Point.__hash__ is None     -> True
   {Point(1, 2)}              -> TypeError: unhashable type: 'Point'
   {Point(1, 2): 'x'}         -> TypeError: unhashable type: 'Point'
   len({HPoint(1, 2), HPoint(1, 2)}) -> 1
   repr(Plain(1, 2)) начинается с -> <__main__.Plain object
   @dataclass: DPoint.__hash__ is None -> True
   @dataclass(frozen=True): len({FPoint(1, 2), FPoint(1, 2)}) -> 1

Логика у правила простая. Хеш по умолчанию считается от тождества объекта — id(), в CPython это адрес, — а словарь и множество требуют, чтобы равные объекты имели равный хеш. Как только равенство стало «по значению», хеш «по адресу» стал бы ему противоречить — и Python его отключает, вместо того чтобы молча сломать словарь. Без __eq__ объекты сравниваются по тождеству: Plain(1, 2) == Plain(1, 2) — False. Если объекту нужно быть ключом, __hash__ пишут по тем же полям, что и __eq__, — тогда множество из двух равных точек содержит одну. И поля эти не должны меняться, пока объект лежит в словаре.

На 3.14 сообщение об ошибке другое и называет роль объекта — cannot use 'Point' as a set element (unhashable type: 'Point') и cannot use 'Point' as a dict key (unhashable type: 'Point'); смысл тот же.

Глубже: __slots__ убирает словарь не всегда

контракт языкаЧто делает __slots__ и когда словарь всё же есть — модель данных (раздел __slots__); вывод — прогон bench/oop/basics.py, раздел 7, добавлен 14.10.2026. Поведение одинаково на CPython 3.11–3.14, текст AttributeError на 3.13 и 3.14 длиннее — версии в истории ниже.

__slots__ — атрибут класса со списком имён, которые будут у экземпляров. Частый пересказ — «у класса со __slots__ нет словаря экземпляра» — верен только при условиях. Документация говорит осторожнее: slots reserves space for the declared variables and prevents the automatic creation of __dict__ and __weakref__ for each instance (резервирует место под объявленные переменные и предотвращает автоматическое создание __dict__ и __weakref__ у каждого экземпляра), и тут же оговаривает: When inheriting from a class without __slots__, the __dict__ and __weakref__ attribute of the instances will always be accessible (при наследовании от класса без __slots__ атрибут __dict__ и __weakref__ экземпляров будет доступен всегда). Второй путь к словарю — перечислить "__dict__" в самих slots.

Вывод bench/oop/basics.py, раздел 7:

7) __slots__: когда у экземпляра нет __dict__, а когда есть
   OnlySlots: hasattr(o, '__dict__') -> False
   OnlySlots: o.extra = 1 -> AttributeError: 'OnlySlots' object has no attribute 'extra' and no __dict__ for setting new attributes
   Child(Base), __slots__ = (): c.extra = 1 -> vars(c) = {'extra': 1}
   Child(Base): hasattr(c, '__dict__') -> True
   __slots__ = ('x', '__dict__'): s.extra = 1 -> vars(s) = {'extra': 1}

Три класса из прогона:

  • OnlySlots со __slots__ = ("x",), базовый класс — только object, и "__dict__" в slots нет. Здесь словаря у экземпляра действительно нет, и присвоить имя, которого нет в slots, нельзя.
  • Child(Base) со __slots__ = (), где Base — обычный класс без slots. Словарь экземпляру даёт родитель: c.extra = 1 проходит и ложится в vars(c).
  • SlotsWithDict с "__dict__" в slots: словарь включён явно.

Отсюда точная формулировка: у класса со __slots__, у которого ни один базовый класс не даёт словаря экземпляра и в slots нет "__dict__", экземпляры не получают __dict__. А сами имена из slots, по документации, are implemented at the class level by creating descriptors for each variable name (реализованы на уровне класса созданием дескрипторов для каждого имени переменной) — это и есть оговорка механизма 2 о дескрипторах; подробно — в уроке о дескрипторах.

Как отвечать на собеседовании

Короткий ответ: класс хранит общие для экземпляров атрибуты и методы, экземпляр обычного класса — свои данные в собственном словаре. Метод — функция в классе, self — её первый аргумент: a.m() равносильно A.m(a). Чтение обычного атрибута ищет в экземпляре, потом в классе; присваивание через экземпляр пишет в экземпляр. @classmethod получает класс, @staticmethod — ничего.

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

На «что такое self». «Обычный первый параметр. a.hello — связанный метод: объект, который помнит функцию и экземпляр и при вызове подставляет экземпляр первым аргументом. У меня a.hello() и Greeter.hello(a) дали одно и то же, а a.hello is a.hello — False: объект каждый раз новый».

На «атрибут класса против атрибута экземпляра» — ловушка. «Список, объявленный в теле класса, один на все экземпляры: три экземпляра добавили по метке — каждый видит три. c1.tags.append — это чтение, а не присваивание. Изменяемое состояние создают в __init__».

На «зачем classmethod». «Он получает класс, через который вызван. Фабрика create с return cls(), объявленная в родителе, через наследника создаёт наследника — у меня Dog.create() вернул Dog».

На «есть ли private». «Нет. _x — соглашение. __x переименовывается в _Класс__x, и это защита от совпадения имён в наследнике, а не от доступа».

На «__init__ — конструктор?». «Нет, объект к этому моменту уже создан __new__. И если переопределить __eq__, а объект должен быть ключом словаря, надо определить и __hash__: иначе он станет None».

На «у класса со __slots__ нет __dict__?». «Только если ни один базовый класс не даёт словаря и "__dict__" нет в slots. Наследник обычного класса со __slots__ = () словарь получает от родителя — у меня c.extra = 1 прошёл, и vars(c) стал {'extra': 1}».

Дальше спросят

Спросят дальше

Чем атрибут класса удобен, если с ним столько ловушек?

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

Неизменяемый атрибут класса — нормальная практика: константа, общая для всех экземпляров (sound = "гав"), или значение по умолчанию, которое экземпляр может переопределить присваиванием. Ловушка только у изменяемых: их меняют на месте, а не присваивают, и изменение достаётся всем.

Спросят дальше

Что такое property и при чём тут атрибуты?

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

@property превращает метод в атрибут, который читается без скобок, но вычисляется кодом: так проверяют значение при записи или считают его на лету, не меняя интерфейса класса. Устроено это через дескрипторы — тот же механизм, что превращает функцию в связанный метод. Разобрано в уроке о дескрипторах.

Спросят дальше

Как сравнить два объекта по значению?

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

Определить __eq__ — и, если объект нужен в словаре или множестве, __hash__ по тем же полям. Для классов-«записей» проще взять @dataclass: он генерирует __init__, __repr__ и __eq__. Хеш у него по умолчанию отключён ровно по правилу выше — в прогоне DPoint.__hash__ это None, — а с frozen=True датакласс генерирует __hash__, и множество из двух равных точек содержит одну.

Спросят дальше

Что будет при множественном наследовании?

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

Атрибут ищется по списку классов __mro__, который строит алгоритм C3, а super() ведёт не к родителю, а к следующему классу в этом списке. Это отдельный урок — о MRO.

Частые заблуждения

Утверждение

self — ключевое слово, и Python передаёт его как-то особо

На самом деле

Это обычный первый параметр. a.hello() и Greeter.hello(a) — один и тот же вызов; связанный метод только подставляет экземпляр первым аргументом. Назвать параметр иначе можно — так просто не принято.

Утверждение

Атрибут, объявленный в теле класса, копируется в каждый экземпляр

На самом деле

Он один на всех и лежит в классе. Пока экземпляр его только читает, разницы не видно; изменяемый атрибут, изменённый на месте (append) через любой экземпляр, меняется у всех — в прогоне c1 и c2 видели все три метки, а в словаре c2 не было ничего.

Утверждение

__init__ — конструктор

На самом деле

Объект создаёт __new__; __init__ получает уже созданный объект и заполняет его. В прогоне __new__ печатает раньше __init__.

Утверждение

Двойное подчёркивание делает атрибут приватным

На самом деле

Оно переименовывает атрибут в _Класс__имя, и по этому имени он читается свободно. Смысл переименования — чтобы атрибут наследника с тем же именем не затёр родительский.

Утверждение

Если определить __eq__, объект можно класть в множество как раньше

На самом деле

Нельзя: __hash__ неявно становится None, и {Point(1, 2)} падает с TypeError: unhashable type. Нужен свой __hash__ по тем же полям.

Утверждение

Если в классе есть __slots__, у экземпляров точно нет __dict__

На самом деле

Только если ни один базовый класс не даёт словаря экземпляра и "__dict__" нет в slots. В прогоне наследник обычного класса со __slots__ = () принял c.extra = 1, и vars(c) стал {'extra': 1}.

История версий

ВерсияИзменениеЧто это значит для кода
3.0Классов старого стиля больше нет: любой класс наследуется от object, и всё, что разобрано в уроке, верно для любого класса. Появляется super() без аргументов (PEP 3135).
3.11Опорная точка урока. Разделы 1–6 скрипта наблюдений на 3.11, 3.12 и 3.13 совпадают построчно. Прогоны — CPython 3.11.15, 3.12.3, 3.13.7, 3.14.7 на linux/amd64.
3.13В разделе 7 (__slots__) удлиняется текст исключения при записи имени, которого нет в slots: 'OnlySlots' object has no attribute 'extra' and no __dict__ for setting new attributes вместо 'OnlySlots' object has no attribute 'extra' на 3.11 и 3.12. Поведение то же; на 3.14 текст как на 3.13.
3.14В выводе скрипта наблюдений меняется только текст исключения для нехешируемого объекта: он называет роль — cannot use 'Point' as a set element (unhashable type: 'Point') и cannot use 'Point' as a dict key (…) вместо unhashable type: 'Point'. Поведение то же. Тесты, сверяющие текст исключения целиком, на 3.14 сломаются.
3.15Вышел 9 октября 2026. Урок на нём не запускался: граница проверки — CPython 3.14.7, сверено 14.10.2026. Правила из документации, на которые опирается урок, приведены по документации 3.13 и 3.14; вывод на 3.15 — мерить у себя.

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.

Практика · что напечатает

Список объявлен в теле класса, метод add дописывает в self.items. Две корзины, в каждую добавили по товару. Что напечатает этот код?
class Cart:
  items = []

  def add(self, item):
      self.items.append(item)


a = Cart()
b = Cart()
a.add("apple")
b.add("pear")
print(len(a.items), len(b.items), a.items is b.items)

Практика · оцените

Объект u, у класса есть обычный метод greet. В список сложили u.greet тысячу раз подряд. Сколько среди них разных объектов?
объектов

Проверьте себя

Вопрос 1 из 4

Чем вызов a.hello() отличается от Greeter.hello(a)?

Чем измерено

Выводы этого урока получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём запускали и что получилось.

Источники и что читать дальше

5 ИСТОЧНИКОВ

  1. Справочник языка — определения классовОфициальная документация. Примечание для программиста, в котором сказано всё главное об атрибутах: «Variables defined in the class definition are class attributes; they are shared by instances. Instance attributes can be set in a method with self.name = value. Both class and instance attributes are accessible through the notation self.name, and an instance attribute hides a class attribute with the same name when accessed in this way. Class attributes can be used as defaults for instance attributes, but using mutable values there can lead to unexpected results» (Переменные, определённые в теле класса, — атрибуты класса; они общие для экземпляров. Атрибуты экземпляра можно задать в методе через self.name = value. И к атрибутам класса, и к атрибутам экземпляра обращаются через self.name, и при таком обращении атрибут экземпляра скрывает одноимённый атрибут класса. Атрибуты класса можно использовать как значения по умолчанию для атрибутов экземпляра, но изменяемые значения там могут привести к неожиданным результатам). Текст сверен по встроенной справке pydoc (модуль pydoc_data.topics) CPython 3.13.7 и 3.14.7.https://docs.python.org/3.13/reference/compound_stmts.html#class-definitions
  2. Модель данных — методы экземпляраОфициальная документация. Что такое self на самом деле: «When an instance method object is called, the underlying function (__func__) is called, inserting the class instance (__self__) in front of the argument list» (Когда вызывается объект метода экземпляра, вызывается лежащая под ним функция (__func__), и экземпляр класса (__self__) вставляется в начало списка аргументов). И дальше: «calling x.f(1) is equivalent to calling C.f(x, 1)» (вызов x.f(1) равносилен вызову C.f(x, 1)). Сверено по pydoc_data.topics 3.13.7 и 3.14.7.https://docs.python.org/3.13/reference/datamodel.html#instance-methods
  3. Модель данных — __init__ и __hash__Официальная документация. Когда вызывается __init__: «Called after the instance has been created (by __new__()), but before it is returned to the caller» (Вызывается после того, как экземпляр создан (методом __new__()), но до того, как он возвращён вызывающему). Про наследование: «If a base class has an __init__() method, the derived class's __init__() method, if any, must explicitly call it to ensure proper initialization of the base class part of the instance» (Если у базового класса есть метод __init__(), то __init__() производного класса, если он есть, обязан явно вызвать его, чтобы правильно инициализировать базовую часть экземпляра). И про хеш: «A class that overrides __eq__() and does not define __hash__() will have its __hash__() implicitly set to None» (У класса, который переопределяет __eq__() и не определяет __hash__(), __hash__() неявно становится None). Зачем вообще переопределяют __new__: «__new__() is intended mainly to allow subclasses of immutable types (like int, str, or tuple) to customize instance creation. It is also commonly overridden in custom metaclasses in order to customize class creation» (__new__() нужен главным образом для того, чтобы наследники неизменяемых типов (вроде int, str или tuple) могли настроить создание экземпляра. Его также часто переопределяют в собственных метаклассах, чтобы настроить создание класса). Сверено по pydoc_data.topics 3.13.7 и 3.14.7.https://docs.python.org/3.13/reference/datamodel.html#object.__init__
  4. Справочник языка — искажение приватных имёнОфициальная документация. Какие имена считаются приватными: «When an identifier that textually occurs in a class definition begins with two or more underscore characters and does not end in two or more underscores, it is considered a private name of that class» (Если идентификатор, встречающийся в определении класса, начинается с двух или более подчёркиваний и не заканчивается двумя или более подчёркиваниями, он считается приватным именем этого класса). Что с ними происходит: «the identifier __spam occurring in a class named Foo, _Foo or __Foo is transformed to _Foo__spam» (идентификатор __spam, встретившийся в классе с именем Foo, _Foo или __Foo, превращается в _Foo__spam). Сверено по pydoc_data.topics 3.13.7 и 3.14.7.https://docs.python.org/3.13/reference/expressions.html#private-name-mangling
  5. Модель данных — __slots__Официальная документация. Что делает __slots__: «__slots__ reserves space for the declared variables and prevents the automatic creation of __dict__ and __weakref__ for each instance» (__slots__ резервирует место под объявленные переменные и предотвращает автоматическое создание __dict__ и __weakref__ у каждого экземпляра). Когда словарь всё же есть: «When inheriting from a class without __slots__, the __dict__ and __weakref__ attribute of the instances will always be accessible» (При наследовании от класса без __slots__ атрибут __dict__ и __weakref__ экземпляров будет доступен всегда) и «If dynamic assignment of new variables is desired, then add '__dict__' to the sequence of strings in the __slots__ declaration» (Если нужно динамически присваивать новые переменные, добавьте '__dict__' в последовательность строк объявления __slots__). Как устроены: «__slots__ are implemented at the class level by creating descriptors for each variable name» (__slots__ реализованы на уровне класса созданием дескрипторов для каждого имени переменной). Сверено по pydoc_data.topics 3.13.7 и 3.14.7, 14.10.2026.https://docs.python.org/3.14/reference/datamodel.html#slots