сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Python: ядро языка

Python: модель данных и ссылки

Зачем это спрашивают

Дандер-методы это и есть «питоничность»: len(), for, with работают через протоколы, и вопросы про __eq__/__hash__ и is против == проверяют, понимаешь ли ты язык изнутри.

// Самый частый провал темы: переопределил __eq__, и объекты внезапно перестали лезть в set. Разберём именно его.

Протоколы: язык зовёт дандеры

len(x) → __len__, x[k] → __getitem__, for → __iter__, with → __enter__/__exit__. Реализуешь протокол - объект ведёт себя как встроенный. Это и отличает питонический дизайн от джавы-на-питоне.

__repr__ - однозначное представление для разработчика (логи, отладка), __str__ - для пользователя. Минимум __repr__ стоит определять всегда: дебажить <Object at 0x…> - самонаказание.

// Истинность: __bool__, иначе __len__, иначе True. Пустые коллекции ложны - if not items: идиоматичнее сравнения длины с нулём.

__repr__ / __str__
для разработчика (однозначно) / для пользователя (красиво)

__eq__ и __hash__ ходят парой; is не равно ==

Контракт: равные объекты обязаны иметь равные хеши. Переопределил __eq__ - Python занулил __hash__, объект выпал из set и ключей dict. Определяй __hash__ по тем же полям, что участвуют в __eq__; мутабельные объекты честнее не хешировать вовсе.

is - идентичность (тот же объект в памяти), == - равенство значений. is уместен для None, True, False и синглтонов, и только для них.

// x is 1000 может быть False при x == 1000: интернирование мелких int - деталь реализации, а не гарантия языка. Сравнивать числа через is - мина.

хеш-контракт
a == b ⇒ hash(a) == hash(b); иначе dict/set ломаются

Контекстные менеджеры и опасные дандеры

with гарантирует __exit__ при любом исходе - исключение, return, что угодно: ресурс освобождён. Свой менеджер - пара __enter__/__exit__ или генератор с @contextlib.contextmanager: код до yield - вход, после - гарантированная уборка.

__getattr__ зовётся только при провале обычного поиска атрибута; __getattribute__ - на каждый доступ, и self.attr внутри него - бесконечная рекурсия (нужен object.__getattribute__).

// Для доменных типов операторы перегружают дандерами (__add__, __lt__), а functools.total_ordering достроит все сравнения из __eq__ + __lt__.

contextmanager
контекстный менеджер из генератора: до yield - вход, после - уборка

Как отвечать: «Переопределил __eq__ - что ещё сломалось и почему?»

Хешируемость: когда класс определяет __eq__ без __hash__, Python выставляет __hash__ = None, и объект больше не годится в set и в ключи dict - TypeError: unhashable. Причина - контракт: равные объекты обязаны давать равный хеш, а старый хеш по identity его нарушил бы. Чиню, определяя __hash__ по тем же полям, что сравнивает __eq__; если объект мутабельный - не хеширую вовсе. Либо отдаю это dataclass'у: eq=True, frozen=True генерируют согласованную пару за меня.

Названо не только «что сломалось», но и почему язык ломает это намеренно - плюс два пути починки, ручной и декларативный.

На чём валят

  • x is 1000 может быть False при равенстве значений - интернирование не гарантия; is только для None и синглтонов.
  • __eq__ без __hash__ - TypeError: unhashable при попытке положить в set.
  • Ресурсы без with: исключение между open и close - утечка.
  • __getattribute__ с self.attr внутри - бесконечная рекурсия.

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

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #data_model1 / 5
    t = ([1],); затем t[0] += [2]. Что произойдёт?
    A)Бросится TypeError (кортеж неизменяем), НО t[0] всё равно станет [1, 2] — список мутировал до неудачного присваивания обратно в кортеж
    B)Ничего не изменится и не произойдёт: кортеж неизменяем, поэтому t[0] спокойно останется равен [1] безо всяких ошибок и предупреждений
    C)Список внутри просто спокойно расширится до [1, 2] безо всякой ошибки — кортеж хранит лишь ссылку, а менять содержимое по ссылке можно
    D)Кортеж целиком заменится на новый ([1, 2],), потому что операция += создаёт новый кортеж на месте старого
    показать ответ и разбор
    +A)Бросится TypeError (кортеж неизменяем), НО t[0] всё равно станет [1, 2] — список мутировал до неудачного присваивания обратно в кортеж

    // разбор: t[0] += [2] для списка означает t[0] = t[0].__iadd__([2]): сначала список расширяется на месте (__iadd__ мутирует его до [1,2] и возвращает тот же объект), а затем Python пытается ПРИСВОИТЬ результат обратно в t[0] — и это присваивание в неизменяемый кортеж падает с TypeError. Итог парадоксален (проверено): исключение брошено, но список УЖЕ стал [1,2]. Отсюда правило: не держать мутабельные объекты в кортежах и не полагаться на += к ним.

  2. #data_model2 / 5
    a = [1, 2, 3]; b = a; b.append(4). Что теперь лежит в a?
    A)[1, 2, 3] — b получил собственную копию списка
    B)ошибка времени выполнения: список менять через второе имя
    C)зависит от области видимости, где объявлены a и b
    D)[1, 2, 3, 4] — a и b указывают на один список
    показать ответ и разбор
    +D)[1, 2, 3, 4] — a и b указывают на один список

    // разбор: Присваивание в Python не копирует объект — оно привязывает имя к тому же объекту. a и b — две ссылки на один список, изменение через любую видно через обе. Нужна копия — list(a), a[:], copy.copy(); для вложенных структур — copy.deepcopy(). Тот же механизм даёт сюрпризы с изменяемыми аргументами функций.

  3. #data_model3 / 5
    Какие из встроенных типов Python неизменяемы (immutable)?
    A)list, dict и set — их содержимое защищено от изменения
    B)Неизменяемых типов среди встроенных нет, менять можно всё
    C)int, str и tuple — их значение нельзя поменять на месте
    D)Только str: строки хранятся в общем пуле и переиспользуются
    показать ответ и разбор
    +C)int, str и tuple — их значение нельзя поменять на месте

    // разбор: Числа, строки, кортежи и frozenset неизменяемы: s += 'x' не правит строку, а создаёт новую и переназначает имя. Списки, словари и множества меняются на месте. Практическое следствие — ключом словаря и элементом множества может быть только хешируемое значение: кортеж подойдёт, список нет. Отсюда же и грабля с изменяемым аргументом по умолчанию.

  4. #data_model4 / 5
    t = (1, 2); затем t += (3,). Кортеж ведь неизменяем — почему код работает?
    A)Кортеж изменяем при добавлении в конец, запрещена лишь замена элемента внутри
    B)Создаётся новый кортеж, и имя t начинает указывать на него
    C)Python молча превращает t в список, чтобы выполнить операцию
    D)Операция выполняется на месте: у кортежей есть метод __iadd__
    показать ответ и разбор
    +B)Создаётся новый кортеж, и имя t начинает указывать на него

    // разбор: Оператор += для неизменяемых типов не правит объект, а собирает новый и переназначает имя — id(t) до и после будет разным. Так же ведут себя строки и числа. Отсюда неочевидное следствие: если тот же кортеж лежал в другой переменной, она по-прежнему указывает на старую версию. Списки же реализуют __iadd__ и меняются на месте, поэтому там += видят все ссылки.

  5. #data_model5 / 5
    Фильтр статусов пропускает вообще все заказы:
    for o in orders:
        if o.status == 'paid' or 'shipped':
            process(o)  # выполняется всегда
    Почему условие всегда истинно?
    A)Оператор or в Python сравнивает строки по длине, а 'shipped' длиннее типичных статусов
    B)Строковые литералы в условии интернируются и совпадают со статусами того же размера
    C)Условие читается как (status == 'paid') or 'shipped': непустая строка истинна сама по себе
    D)== имеет более низкий приоритет, чем or, поэтому сравнивается уже результат целого or-выражения
    показать ответ и разбор
    +C)Условие читается как (status == 'paid') or 'shipped': непустая строка истинна сама по себе

    // разбор: or не «дораспределяет» сравнение на второй операнд: правая часть — просто строка 'shipped', а непустая строка истинна. Итог — условие всегда True. Правильно: o.status == 'paid' or o.status == 'shipped', а идиоматичнее — o.status in ('paid', 'shipped'). Ошибка коварна тем, что код валиден, линтеры старых настроек молчат, а фильтр «работает» — просто пропускает всё.

дальше

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

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