ООП в Python: классы, наследование, dunder-методы
ООП-вопросы по Python - не «расскажи про инкапсуляцию», а специфика языка: чем classmethod отличается от staticmethod, почему список в атрибуте класса общий на всех, когда dataclass. Проверяют язык, а не учебник.
// Питон-ООП держится на трёх китах: явный self, соглашения вместо запретов, композиция чаще наследования.
Атрибуты класса, инстанса и три вида методов
Атрибуты класса общие для всех инстансов, атрибуты инстанса - свои. Мутабельный атрибут класса - список, словарь это общее состояние: все объекты пишут в один список, и это почти всегда баг.
self - просто первый аргумент метода, никакой магии. classmethod получает cls это фабрики: from_config, from_json; при наследовании честно вернут подкласс. staticmethod не получает ничего - утилита, прибитая к неймспейсу класса.
// Инкапсуляция - соглашением: _protected значит «не трогай снаружи», __private - name mangling против случайного переопределения, а не секретность.
- classmethod
- метод с cls вместо self; идиома альтернативных конструкторов
Наследование, MRO и утиная типизация
super().__init__() обязателен, если у родителя есть своя инициализация, иначе родительские поля молча не существуют. При множественном наследовании порядок поиска методов определяет MRO (method resolution order) - линеаризация C3.
Идиома языка - утиная типизация: важно поведение, а не тип. Лестница isinstance-проверок - запах; правильный ход - общий интерфейс или Protocol.
// Композиция чаще лучше наследования: «содержит» гибче, чем «является». Наследоваться ради переиспользования пары методов - строить хрупкую иерархию на ровном месте.
- MRO
- порядок поиска методов (C3); смотрится через Class.__mro__
dataclass и property
dataclass генерирует __init__, __repr__ и __eq__ из аннотаций - стандарт для записей-данных. frozen=True даёт иммутабельность (и хешируемость), мутабельные дефолты - только через field(default_factory=list): default=[] - та же классовая ловушка.
property - вычисляемый атрибут с интерфейсом поля: снаружи obj.total, внутри - вычисление или валидация в сеттере. Позволяет добавить логику, не ломая API.
// Практическое правило: данные без поведения - dataclass; понадобилась инвариант-валидация - property; сложная логика - обычный класс.
- field(default_factory=…)
- фабрика мутабельного дефолта в dataclass
Как отвечать: «Чем classmethod отличается от staticmethod и когда что брать?»
classmethod получает класс первым аргументом это идиома альтернативных конструкторов: User.from_config, from_json; при наследовании такой конструктор корректно вернёт подкласс, потому что работает через cls. staticmethod не получает ни инстанс, ни класс - просто функция, прибитая к неймспейсу класса для смысловой группировки. Правило выбора: нужен доступ к классу - classmethod; не нужен - staticmethod; а если метод не трогает ни класс, ни инстанс - часто честнее вынести его функцией модуля.
Различие через применение (фабрики), нюанс наследования и правило с неожиданным третьим вариантом это глубже, чем пересказ сигнатур.
На чём валят
- −Список как атрибут класса - все инстансы пишут в один список.
- −Забыть super().__init__() - родительские поля молча не инициализированы.
- −dataclass с default=[] - мутабельный дефолт; нужен field(default_factory=list).
- −Переопределить __eq__ без __hash__ - объект перестал быть хешируемым.
- −Наследование ради пары методов - композиция проще и честнее.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 22, остальные разбираются в тренажёре.
- Что делает вызов super().__init__(...) в конструкторе класса-наследника?A)Создаёт совершенно новый независимый объект родительского класса и никак не связывает его с текущим экземпляром наследникаB)Вызывает инициализатор родителя для текущего объекта, чтобы родительская часть его состояния тоже корректно настроиласьC)Заменяет собственный __init__ наследника родительским, из-за чего свои поля наследника не инициализируютсяD)Автоматически копирует все методы родителя в наследника — хотя при обычном наследовании это и так происходит само собой
показать ответ и разбор
+B)Вызывает инициализатор родителя для текущего объекта, чтобы родительская часть его состояния тоже корректно настроилась// разбор: super().__init__() выполняет инициализатор родителя применительно к тому же self, чтобы унаследованная часть объекта (родительские поля) настроилась. Если наследник определил свой __init__ и не вызвал super, родительская инициализация не выполнится, и объект окажется недонастроен. Это про инициализацию текущего объекта, а не про создание нового.
- Зачем классу определять методы __repr__ и __eq__?A)Чтобы ускорить создание экземпляров класса, поскольку эти методы оптимизируют выделение памятиB)Чтобы запретить наследование от класса и защитить его реализацию от переопределения в потомкахC)Это обязательные методы, без определения которых Python вообще не даст создать ни одного экземпляра такого класса в кодеD)__repr__ даёт читаемое представление для отладки и логов, __eq__ — сравнение объектов по значению вместо сравнения по ссылке
показать ответ и разбор
+D)__repr__ даёт читаемое представление для отладки и логов, __eq__ — сравнение объектов по значению вместо сравнения по ссылке// разбор: По умолчанию __repr__ печатает малополезное <Object at 0x...>, а __eq__ сравнивает по идентичности (как is). Свой __repr__ делает объект понятным в логах и отладке, свой __eq__ сравнивает по содержимому — что обычно и нужно. dataclass генерирует оба автоматически. Не обязательны, но резко повышают удобство работы с объектом.
- Чем classmethod, staticmethod и обычный метод отличаются по неявному первому аргументу?A)Ничем: все три вида методов получают первым аргументом сам экземпляр self и ведут себя одинаковоB)Classmethod получает self, staticmethod получает cls, а обычный метод не получает вообще никакого неявного первого аргументаC)Обычный метод получает экземпляр (self), classmethod — класс (cls), staticmethod — ничего неявного, это просто функция в namespace классаD)Staticmethod получает и self, и cls сразу, обычный метод — self, а classmethod не получает ни того, ни другого
показать ответ и разбор
+C)Обычный метод получает экземпляр (self), classmethod — класс (cls), staticmethod — ничего неявного, это просто функция в namespace класса// разбор: Обычный метод получает экземпляр через self и работает с состоянием объекта. classmethod получает класс через cls — удобно для альтернативных конструкторов и работы с атрибутами класса. staticmethod не получает ничего неявного: это просто сгруппированная в классе функция. Выбор диктуется тем, что методу реально нужно — объект, класс или ничего.
- Зачем оборачивать доступ к полю в @property вместо публичного атрибута?A)Позволяет вычислять или валидировать значение при доступе, сохранив синтаксис obj.x — логику можно ужесточить, не меняя APIB)@property делает атрибут приватным и недоступным для чтения снаружи класса без специального пароляC)@property ускоряет доступ к атрибуту, кешируя его значение в C-структуре объекта в обход обычного механизма словаря __dict__D)@property автоматически сохраняет каждое присвоенное полю значение в базу данных без написания какого-либо кода вручную вообще
показать ответ и разбор
+A)Позволяет вычислять или валидировать значение при доступе, сохранив синтаксис obj.x — логику можно ужесточить, не меняя API// разбор: property превращает метод в атрибут-с-логикой: доступ obj.x вызывает геттер (можно вычислять на лету или сделать поле read-only), присваивание — сеттер (можно валидировать). При этом внешний код по-прежнему пишет obj.x. Главная ценность — начать с простого публичного атрибута и позже добавить логику, не ломая интерфейс. Приватность и БД тут ни при чём.
- class D(B, C), где и B, и C наследуют A и оба определяют метод who. Чей who вызовет D().who() и почему?A)A.who — Python при множественном наследовании в первую очередь идёт к общему предку A и берёт метод у негоB)Оба сразу — и B.who, и C.who: их возвращаемые результаты Python автоматически объединяет между собой в один общий ответ методаC)B.who — по порядку MRO (C3-линеаризация): D → B → C → A; побеждает первый в списке баз, где метод найденD)Возникнет ошибка неоднозначности: при таком множественном наследовании Python не может сам выбрать между методами B и C
показать ответ и разбор
+C)B.who — по порядку MRO (C3-линеаризация): D → B → C → A; побеждает первый в списке баз, где метод найден// разбор: Python разрешает методы по MRO (Method Resolution Order), вычисляемому алгоритмом C3-линеаризации. Для class D(B, C) порядок — D → B → C → A → object: поиск идёт слева направо по базам, и вызывается первый найденный who — то есть B.who (проверено). Ошибки неоднозначности нет (C3 даёт единый детерминированный порядок), методы не объединяются, а к общему предку A Python идёт в последнюю очередь. Порядок баз в объявлении класса важен.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.