Классы и ООП: 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.
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 — обычный первый аргумент
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: атрибут класса и атрибут экземпляра
Документация формулирует правило в одном абзаце:
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: три вида методов
Методы различаются тем, что получают первым аргументом:
| метод | первый аргумент | для чего |
|---|---|---|
| обычный | экземпляр (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__, родительский сам не вызовется:
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__() производного класса, если он есть, обязан явно вызвать его, чтобы правильно инициализировать базовую часть экземпляра.
Вывод 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 — соглашение, а не защита
Модификаторов доступа в Python нет. Есть два соглашения об именах:
_x— «это внутреннее, снаружи не трогайте». Интерпретатор с этим ничего не делает: атрибут доступен как любой другой.__xвнутри класса переименовывается (кроме имён, которые и заканчиваются двумя подчёркиваниями, — это магические методы вроде__init__) —the identifier
(идентификатор __spam, встретившийся в классе с именем Foo, _Foo или __Foo, превращается в _Foo__spam).__spamoccurring in a class namedFoo,_Fooor__Foois transformed to_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__ отключает хеш
Два магических метода, вокруг которых на собеседовании чаще всего ошибаются.
__init__ получает уже созданный объект. Документация: called after the instance has been created (by
(вызывается после того, как экземпляр создан (методом __new__()), но до того, как он возвращён вызывающему). Конструктор в
привычном смысле — пара: __new__()), but before it is returned to the caller__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.
Вывод 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__ нет словаря экземпляра» — верен
только при условиях. Документация говорит осторожнее: slots reserves space for the declared variables and prevents the automatic creation of
(резервирует место под объявленные переменные и предотвращает автоматическое создание __dict__ и __weakref__ у каждого экземпляра), и тут же оговаривает: __dict__ and __weakref__ for each instanceWhen inheriting from a class without
(при наследовании от класса без __slots__ атрибут __dict__ и __weakref__ экземпляров будет доступен всегда). Второй путь к словарю — перечислить __slots__, the __dict__ and __weakref__ attribute of the instances will always be accessible"__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 — мерить у себя. |
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
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)Практика · оцените
Проверьте себя
Чем вызов a.hello() отличается от Greeter.hello(a)?
Чем измерено
Выводы этого урока получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём запускали и что получилось.
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Класс в 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.
На самом деле
- Это обычный первый параметр.
a.hello()иGreeter.hello(a)— один и тот же вызов; связанный метод только подставляет экземпляр первым аргументом. Назвать параметр иначе можно — так просто не принято. - Он один на всех и лежит в классе. Пока экземпляр его только читает, разницы не видно; изменяемый атрибут, изменённый на месте (
append) через любой экземпляр, меняется у всех — в прогонеc1иc2видели все три метки, а в словареc2не было ничего. - Объект создаёт
__new__;__init__получает уже созданный объект и заполняет его. В прогоне__new__печатает раньше__init__. - Оно переименовывает атрибут в
_Класс__имя, и по этому имени он читается свободно. Смысл переименования — чтобы атрибут наследника с тем же именем не затёр родительский. - Нельзя:
__hash__неявно становитсяNone, и{Point(1, 2)}падает сTypeError: unhashable type. Нужен свой__hash__по тем же полям. - Только если ни один базовый класс не даёт словаря экземпляра и
"__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 — мерить у себя.<
Что разобрано
- База: класс — это объект с общими атрибутами
- Механизм 1: `self` — обычный первый аргумент
- Механизм 2: атрибут класса и атрибут экземпляра
- Механизм 3: три вида методов
- Механизм 4: наследование и `super().__init__()`
- Механизм 5: `_x` и `__x` — соглашение, а не защита
- Глубже: `__init__` — не конструктор, а `__eq__` отключает хеш
- Глубже: `__slots__` убирает словарь не всегда
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- История версий
- Практика
- Проверьте себя
- Чем измерено
Источники и что читать дальше
5 ИСТОЧНИКОВ
- Справочник языка — определения классовОфициальная документация. Примечание для программиста, в котором сказано всё главное об атрибутах: «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
- Модель данных — методы экземпляраОфициальная документация. Что такое 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
- Модель данных — __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__
- Справочник языка — искажение приватных имёнОфициальная документация. Какие имена считаются приватными: «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
- Модель данных — __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