сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Асинхронность в Python

ООП в 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, остальные разбираются в тренажёре.

  1. #oop1 / 5
    Что делает вызов super().__init__(...) в конструкторе класса-наследника?
    A)Создаёт совершенно новый независимый объект родительского класса и никак не связывает его с текущим экземпляром наследника
    B)Вызывает инициализатор родителя для текущего объекта, чтобы родительская часть его состояния тоже корректно настроилась
    C)Заменяет собственный __init__ наследника родительским, из-за чего свои поля наследника не инициализируются
    D)Автоматически копирует все методы родителя в наследника — хотя при обычном наследовании это и так происходит само собой
    показать ответ и разбор
    +B)Вызывает инициализатор родителя для текущего объекта, чтобы родительская часть его состояния тоже корректно настроилась

    // разбор: super().__init__() выполняет инициализатор родителя применительно к тому же self, чтобы унаследованная часть объекта (родительские поля) настроилась. Если наследник определил свой __init__ и не вызвал super, родительская инициализация не выполнится, и объект окажется недонастроен. Это про инициализацию текущего объекта, а не про создание нового.

  2. #oop2 / 5
    Зачем классу определять методы __repr__ и __eq__?
    A)Чтобы ускорить создание экземпляров класса, поскольку эти методы оптимизируют выделение памяти
    B)Чтобы запретить наследование от класса и защитить его реализацию от переопределения в потомках
    C)Это обязательные методы, без определения которых Python вообще не даст создать ни одного экземпляра такого класса в коде
    D)__repr__ даёт читаемое представление для отладки и логов, __eq__ — сравнение объектов по значению вместо сравнения по ссылке
    показать ответ и разбор
    +D)__repr__ даёт читаемое представление для отладки и логов, __eq__ — сравнение объектов по значению вместо сравнения по ссылке

    // разбор: По умолчанию __repr__ печатает малополезное <Object at 0x...>, а __eq__ сравнивает по идентичности (как is). Свой __repr__ делает объект понятным в логах и отладке, свой __eq__ сравнивает по содержимому — что обычно и нужно. dataclass генерирует оба автоматически. Не обязательны, но резко повышают удобство работы с объектом.

  3. #oop3 / 5
    Чем 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 не получает ничего неявного: это просто сгруппированная в классе функция. Выбор диктуется тем, что методу реально нужно — объект, класс или ничего.

  4. #oop4 / 5
    Зачем оборачивать доступ к полю в @property вместо публичного атрибута?
    A)Позволяет вычислять или валидировать значение при доступе, сохранив синтаксис obj.x — логику можно ужесточить, не меняя API
    B)@property делает атрибут приватным и недоступным для чтения снаружи класса без специального пароля
    C)@property ускоряет доступ к атрибуту, кешируя его значение в C-структуре объекта в обход обычного механизма словаря __dict__
    D)@property автоматически сохраняет каждое присвоенное полю значение в базу данных без написания какого-либо кода вручную вообще
    показать ответ и разбор
    +A)Позволяет вычислять или валидировать значение при доступе, сохранив синтаксис obj.x — логику можно ужесточить, не меняя API

    // разбор: property превращает метод в атрибут-с-логикой: доступ obj.x вызывает геттер (можно вычислять на лету или сделать поле read-only), присваивание — сеттер (можно валидировать). При этом внешний код по-прежнему пишет obj.x. Главная ценность — начать с простого публичного атрибута и позже добавить логику, не ломая интерфейс. Приватность и БД тут ни при чём.

  5. #oop5 / 5
    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 идёт в последнюю очередь. Порядок баз в объявлении класса важен.

дальше

Теорию прочитали. Навык ставится повторением

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