junior: база, с которой начинают
Чем атрибут класса отличается от атрибута экземпляра? Где каждый из них хранится и что произойдёт, если имена совпадают?
как ответить Обычный атрибут класса хранится в пространстве имён класса, а обычный атрибут экземпляра — в его __dict__. Стандартный поиск obj.x идёт так: data descriptor в MRO, атрибут экземпляра, non-data descriptor или обычный атрибут класса, затем __getattr__(). Поэтому атрибут экземпляра затеняет обычный атрибут класса, но не data descriptor. Обычно self.x = value пишет в __dict__ экземпляра, однако запись могут перехватить __setattr__(), property с сеттером или slot-дескриптор; у объекта со __slots__ своего __dict__ может не быть.
разбор Класс — объект со своим пространством имён; обычное значение, присвоенное в теле класса, разделяется экземплярами, пока конкретный объект не затенит его своим значением. Но полное правило чтения важнее упрощения «сначала экземпляр»: data descriptor — например property с __set__ — имеет приоритет над obj.__dict__. Затем ищется словарь экземпляра, после него non-data descriptor и обычный атрибут по MRO.
Типичные ловушки:
- мутабельный атрибут класса вроде списка общий:
self.items.append(...) мутирует найденный объект и не создаёт собственный атрибут; obj.counter += 1 создаёт значение экземпляра только при обычном атрибуте и стандартном __setattr__; descriptor может перехватить запись;- аннотация
x: int без присваивания попадает в __annotations__, но сама значения не создаёт; vars(obj) и obj.__dict__ доступны не каждому объекту: класс с __slots__ может не иметь словаря.
del obj.x открывает одноимённое значение класса только для обычного словарного атрибута. Descriptor или slot управляет удалением своим протоколом, поэтому это тоже не универсальное правило.
чтобы прозвучать сильнее Назови полный порядок поиска, различие data/non-data descriptors и то, что __getattribute__() и __setattr__() могут изменить стандартное поведение.
python-oop-001 · junior · high Расскажи, что на самом деле делает super() и зачем он нужен — почему не вызвать метод родителя напрямую, например Parent.method(self)?
как ответить super() возвращает прокси-объект, который ищет метод не «в родителе», а в следующем классе по MRO текущего объекта. Это избавляет от хардкода имени базового класса и, главное, корректно работает при множественном наследовании: каждый метод в цепочке выполняется ровно один раз и в правильном порядке. Прямой вызов Parent.method(self) эту кооперативную цепочку ломает — в ромбовидной иерархии общий предок вызовется дважды.
разбор super() не «вызывает родителя» — он возвращает прокси, который начинает поиск атрибута со следующего класса после текущего в type(self).__mro__. В одиночном наследовании это совпадает с родителем, поэтому у многих складывается неверная ментальная модель. Разница видна в ромбовидных иерархиях: при прямых вызовах Parent.method(self) общий предок выполнится дважды (каждая ветка вызовет его сама), а со super() каждый класс из MRO отработает ровно один раз.
Форма без аргументов — работа компилятора: внутри тела класса он кладёт ссылку __class__ в замыкание метода, и super() разворачивается в super(__class__, self).
Типичные ловушки: super().__init__() не вызывается автоматически — забыл, и родительская инициализация молча не произошла; super-прокси нельзя подсовывать в isinstance; в classmethod super тоже работает и передаст дальше cls.
Follow-up почти всегда один: «а кого вызовет super() в классе B, если B участвует в ромбе?» Правильный ответ — «следующего по MRO, возможно, сиблинга C, о котором B вообще не знает», и это не баг, а суть кооперативного наследования.
чтобы прозвучать сильнее Скажи явно, что super() смотрит на MRO типа объекта (type(self)), а не класса, где написан вызов — поэтому super() в B может вызвать метод C, о котором B не знает. Упомяни двухаргументную форму super(Cls, obj) и то, что нулевая форма — сахар на основе ячейки __class__.
python-oop-002 · junior · high Вот классический «ромб». Что выведет этот код и почему именно в таком порядке?
class A:
def hello(self):
print("A")
class B(A):
def hello(self):
print("B")
super().hello()
class C(A):
def hello(self):
print("C")
super().hello()
class D(B, C):
def hello(self):
print("D")
super().hello()
D().hello()
как ответить Выведет D, B, C, A. Порядок задаёт MRO класса D: D → B → C → A → object. Ключевой момент — super() в B вызывает не своего родителя A, а следующий класс по MRO объекта, то есть C, хотя B про C ничего не знает. Именно поэтому со super() общий предок A выполняется один раз, а не дважды.
полный разбор и проверка ответа арбитром — в приложении
python-oop-003 · junior · high Чем __repr__ отличается от __str__ и какой из них ты реализуешь в первую очередь?
как ответить __repr__ — однозначное представление для разработчика: логи, отладчик, REPL; в идеале строка, по которой объект можно воссоздать, вида Money(100). __str__ — читаемый текст для пользователя, его вызывают print и str(). Если __str__ не определён, Python падает обратно на __repr__, поэтому в первую очередь реализуют именно __repr__ — он покрывает оба случая.
полный разбор и проверка ответа арбитром — в приложении
python-oop-004 · junior · high Как устроен with под капотом? Что должны делать __enter__ и __exit__, и что значит возвращаемое значение __exit__?
как ответить with вызывает у объекта __enter__, его результат попадает в переменную после as, а по выходу из блока — всегда вызывает __exit__, даже при исключении. __exit__ получает тип, значение и traceback исключения (или три None, если его не было). Если __exit__ вернёт истинное значение — исключение подавляется, если False/None — летит дальше. Это по сути try/finally, упакованный в протокол.
полный разбор и проверка ответа арбитром — в приложении
python-oop-005 · junior · high Расскажи, что такое @property и зачем он нужен. Когда ты его используешь, а когда обычного атрибута достаточно?
как ответить @property превращает метод в вычисляемый атрибут: снаружи это выглядит как обычное поле, но при чтении выполняется код. Через @x.setter и @x.deleter можно навесить логику на запись и удаление — например, валидацию. Главная ценность — эволюция без слома интерфейса: в Python не пишут геттеры/сеттеры заранее, начинают с публичного атрибута и переводят его в property, только когда понадобилась логика.
полный разбор и проверка ответа арбитром — в приложении
python-oop-006 · junior · high Чем @classmethod отличается от @staticmethod и когда на практике берёшь каждый из них?
как ответить classmethod получает первым аргументом класс (cls) и корректно работает с наследованием — это стандартный способ делать альтернативные конструкторы вроде from_json: cls(...) создаст именно тот класс, от которого вызвали. staticmethod не получает ни self, ни cls — это обычная функция, положенная в класс ради пространства имён. Нужен доступ к классу — classmethod; логика лишь тематически относится к классу — staticmethod или вовсе функция уровня модуля.
полный разбор и проверка ответа арбитром — в приложении
python-oop-007 · junior · high Расскажи про dataclasses: что даёт декоратор @dataclass, когда ты его используешь и какие там классические грабли?
как ответить @dataclass — кодогенератор: по аннотациям полей создаёт __init__, __repr__ и __eq__, опционально ordering, frozen, slots, kw_only. Беру для классов-носителей данных — DTO, конфиги, результаты — вместо ручного бойлерплейта. Важно: типы в рантайме он не проверяет, это не pydantic. Классическая ловушка — изменяемый дефолт: tags: list = [] даёт ValueError, нужен field(default_factory=list).
полный разбор и проверка ответа арбитром — в приложении
python-oop-008 · junior · high Посмотри на код: что выведут оба print и почему?
class Basket:
items = []
def add(self, item):
self.items.append(item)
a = Basket()
b = Basket()
a.add("apple")
print(b.items)
b.items = ["pear"]
b.add("fig")
print(a.items, b.items)
как ответить Первый print выведет ['apple']: items — атрибут класса, общий список, и a.add мутирует именно его. Затем b.items = ["pear"] создаёт у b собственный атрибут экземпляра, который затеняет классовый, и b.add уже дописывает в него. Второй print: ['apple'] ['pear', 'fig'] — у a по-прежнему виден классовый список, у b — свой.
полный разбор и проверка ответа арбитром — в приложении
python-oop-009 · junior · medium Что такое self в методах? Почему его нужно писать явно и что будет, если вызвать метод через класс, а не через экземпляр?
как ответить self — это первый параметр метода, в который попадает сам экземпляр; никакой магии в имени нет, это просто соглашение. При обращении obj.method() Python создаёт bound method, где экземпляр уже подставлен первым аргументом. Через класс метод — обычная функция: Basket.add(a, "x") работает, но экземпляр надо передать руками.
полный разбор и проверка ответа арбитром — в приложении
python-oop-010 · junior · medium middle: где отделяют уверенных
Чем __new__ отличается от __init__? Когда реально приходится переопределять __new__?
как ответить __new__ создаёт и возвращает экземпляр — принимает cls и работает до появления объекта; __init__ получает уже созданный self и только наполняет его, возвращая None. Вызов Class(...) — это по сути cls.__new__(cls, ...), и если результат — экземпляр cls, на нём вызывается __init__ с теми же аргументами. Переопределять __new__ нужно при наследовании от иммутабельных типов (str, int, tuple), для синглтонов/кэша экземпляров и в метаклассах.
полный разбор и проверка ответа арбитром — в приложении
python-oop-013 · middle · high Как Python строит MRO при множественном наследовании? Расскажи про C3-линеаризацию: какие гарантии она даёт и в каком случае класс вообще не создастся.
как ответить MRO — это линейный порядок поиска атрибутов, который C3 строит слиянием MRO родителей и списка баз. Гарантии три: класс всегда идёт раньше любого своего предка, порядок баз из объявления сохраняется, и линеаризация монотонна — относительный порядок двух классов одинаков во всех MRO иерархии. Если условия несовместимы, класс не создаётся: TypeError «Cannot create a consistent MRO». Посмотреть порядок — Cls.__mro__ или Cls.mro().
полный разбор и проверка ответа арбитром — в приложении
python-oop-014 · middle · high Ты определил в классе __eq__ — и объекты перестали складываться в set. Почему так происходит и как правильно реализовать пару __eq__/__hash__?
как ответить Когда в классе определён __eq__ без __hash__, Python выставляет __hash__ = None — объект становится unhashable, чтобы не нарушился контракт: равные объекты обязаны иметь равный хеш, а дефолтный хеш по id() этого не гарантирует. Правильно — определить __hash__ по тем же полям, что участвуют в __eq__, обычно hash(кортежа полей), и держать эти поля неизменяемыми. Иначе объект «потеряется» в set или dict после мутации.
полный разбор и проверка ответа арбитром — в приложении
python-oop-015 · middle · high Чем __getattr__ отличается от __getattribute__? Когда берёшь один, когда другой?
как ответить __getattribute__ вызывается на каждый доступ к атрибуту без исключений, а __getattr__ — только как fallback, когда обычный поиск не нашёл имя, то есть когда __getattribute__ бросил AttributeError. Для делегирования, прокси и ленивой подгрузки берут __getattr__. __getattribute__ переопределяют очень редко: он на горячем пути, и любое self.x внутри него даёт бесконечную рекурсию — обращаться нужно через object.__getattribute__.
полный разбор и проверка ответа арбитром — в приложении
python-oop-016 · middle · high ABC против typing.Protocol: чем они отличаются, как это связано с утиной типизацией и когда ты берёшь что?
как ответить ABC — номинальная типизация: реализация явно наследуется от базы, abstractmethod не даст создать инстанс без реализации — контракт проверяется в рантайме при инстанцировании. Protocol — структурная: класс подходит, если у него просто есть нужные методы, наследоваться не надо, а проверяет в основном статический тайпчекер. ABC беру для своей иерархии, где нужен жёсткий контракт и общий код в базе; Protocol — чтобы описать требования к чужим объектам, которые я не контролирую: это формализованная утиная типизация.
полный разбор и проверка ответа арбитром — в приложении
python-oop-017 · middle · high