Вопросы на собеседовании Python: итераторы, состояние и исключения
Quick Overview
Авторский импорт JSON-событий: точный trace next/yield, независимость состояния, алиасы словаря, исключения и cleanup с фактическими38 проверками.
Функция импорта вернула генератор, но источник ещё не прочитан. После двух успешных next() третья строка вызывает исключение. Какие события уже выданы, закрылся ли источник и можно ли снова обойти тот же генератор? Эти вопросы проверяют понимание Python точнее, чем отдельные определения итератора и контекстного менеджера.
Проследите импорт событий из строк JSON от создания генератора до ошибки и закрытия. В нём есть ленивое чтение, состояние дедупликации, проверка типов и явный владелец закрытия. Следующий шаг — Implement a robust Python generator: попробуйте обосновать каждую гарантию конкретным порядком вызовов.
Граница доказательств: правила языка и стандартной библиотеки сверены с официальной документацией Python3.12. Код, данные и критерии проверки авторские. В CPython3.12.14 прошли38 контролей; источник в памяти считает чтения и закрытие. Это не тест файловых дескрипторов, сети, базы данных или производительности. Отчёты кандидатов не используются, а набор не приписывается реальному работодателю.

Контракт события и границы импорта
Каждая строка должна содержать JSON-объект ровно с полями id и delta. id — непустая строка; delta — целое число, причём bool не принимается. Значения могут быть положительными, отрицательными и нулевыми. Повтор id внутри одного обхода считается ошибкой, даже если содержимое совпало. Это авторская политика, а не универсальное правило обработки событий.
Четыре входные строки выглядят так:
{"id":"e1","delta":2}
{"id":"e2","delta":-1}
{"id":"e3","delta":"oops"}
{"id":"e4","delta":4}
Третья строка — синтаксически корректный JSON с неправильным типом delta. Ошибка возникает при её потреблении, а не при создании генератора. Четвёртая строка не должна быть прочитана после этой ошибки. Два первых события уже выданы вызывающему коду; их получение не отменяется автоматически.
Контракт не задаёт максимальную длину id, ограничение целого числа и правила пробелов внутри идентификатора. Не обещайте такую валидацию по одному слову «строгая». Если продукт требует ограничение размера записи, это отдельное условие до разбора или внутри него.
Итерируемый объект, итератор и объект генератора
Официальный учебник Python об итераторах и генераторах описывает вызовы iter и next, остановку через StopIteration и приостановку выполнения на yield. В нашем случае вызов функции events создаёт объект генератора; тело начнёт работу при первом продвижении.
source = Source(lines)
g = events(source)
assert source.entered == 0
assert source.reads == 0
assert iter(g) is g
first = next(g) # Event('e1', 2)
assert source.entered == 1
assert source.reads == 1
Source — тестовый контекстный менеджер. Его __enter__ увеличивает entered, __iter__ увеличивает reads перед каждой выдачей строки, а __exit__ устанавливает closed. В полном лабораторном коде это обычный объект в памяти. Название Source не должно создавать впечатление, что здесь действительно открывался файл.
Список допускает новый обход; уже исчерпанный объект генератора не запускает функцию заново. На валидных двух строках первый list(g) дал два события, второй — пустой список. После ошибки наш генератор также завершился: следующий next вызвал StopIteration. Для нового импорта нужен новый объект генератора и пригодный для нового чтения источник.
Не называйте генераторы просто «функциями, экономящими память». Генераторная функция создаёт объект, который хранит состояние приостановленного выполнения. Расход памяти зависит и от того, что сохраняют источник, локальные переменные и потребитель.
Реализация: что происходит до каждого yield
Ниже основная функция лаборатории. Event — frozen dataclass с двумя полями, event_id:str и delta:int. Он создаётся заново для каждой принятой строки.
def events(source):
seen = set()
with source as lines:
for number, line in enumerate(lines, 1):
try:
raw = json.loads(line)
except json.JSONDecodeError as exc:
raise ValueError(f'line {number}: invalid JSON') from exc
if type(raw) is not dict or set(raw) != {'id', 'delta'}:
raise ValueError(f'line {number}: fields')
if type(raw['id']) is not str or not raw['id']:
raise ValueError(f'line {number}: id')
if type(raw['delta']) is not int:
raise ValueError(f'line {number}: delta')
if raw['id'] in seen:
raise ValueError(f'line {number}: duplicate id')
seen.add(raw['id'])
yield Event(raw['id'], raw['delta'])
seen принадлежит этому запуску тела генератора. Проверка id предшествует добавлению; выдача события происходит после всех проверок данной строки. Если ошибка случится позже, предыдущие yield не отзываются. Аннотации Event помогают читать контракт, но сами по себе не проверяют JSON во время выполнения.
Почему здесь type(delta) is int, а не isinstance? Это намеренное исключение bool из нашего формата. Проверка фактического входа true завершилась ValueError. Она не означает, что exact-type проверки всегда лучше поддержки подклассов; решение зависит от разрешённого формата входа.
По официальной документации dataclasses, frozen ограничивает присваивание полям экземпляра. Это не обещание глубокой неизменяемости произвольного графа объектов. У нашего события строка и целое число, а не изменяемая вложенная коллекция.
Предскажите состояние после третьего next
| Действие | entered | reads | Результат | closed |
|---|---|---|---|---|
g = events(source) | 0 | 0 | Создан объект генератора | false |
Первый next(g) | 1 | 1 | Event('e1', 2) | false |
Второй next(g) | 1 | 2 | Event('e2', -1) | false |
Третий next(g) | 1 | 3 | ValueError('line 3: delta') | true |
Следующий next(g) | 1 | 3 | StopIteration | true |
Счётчики entered/reads, значения событий и флаг closed в таблице сверены с выполнением CPython3.12.14. В момент первых двух yield контекст with остаётся активным, поэтому closed=false. Ошибка третьей строки выходит из контекста и запускает его завершение. До e4 выполнение не доходит.
Теперь уточните требование: допускается ли частичный результат? Если потребитель уже записал e1 и e2 во внешнюю систему, этот генератор не умеет их откатить. Если потребителю нужна сначала проверенная целая партия, он может собрать локальный список до применения эффектов. Это другой режим потребления с другим расходом памяти.
В проверке staged = list(events(source)) при ошибке оставило прежнее значение staged равным None: правая часть не завершилась. Такая наблюдаемая семантика присваивания не равна транзакции базы данных. Внешние эффекты, сделанные самим источником или потребителем, требуют отдельной стратегии.
Общий изменяемый аргумент: ошибка между вызовами
Официальный раздел Python об аргументах по умолчанию объясняет, что значение по умолчанию вычисляется при определении функции. Поэтому множество в следующем примере разделяется вызовами:
def remember_bad(event_id, seen=set()):
fresh = event_id not in seen
seen.add(event_id)
return fresh
print(remember_bad('x')) # True
print(remember_bad('x')) # False
Это может ошибочно связать два независимых импорта. Замена на None позволяет создавать новое множество только тогда, когда вызывающий код не передал своё состояние:
def remember_fixed(event_id, seen=None):
if seen is None:
seen = set()
fresh = event_id not in seen
seen.add(event_id)
return fresh
Два отдельных вызова remember_fixed('x') дали True, True. С явно переданным одним множеством результат снова True, False — теперь совместное владение видно в аргументе. Исправление не означает, что каждый вызов должен забывать историю: для дедупликации всей партии состояние должно сохраняться внутри одной партии, как seen в events.
На собеседовании сначала скажите, кому принадлежит множество и как долго оно живёт. Только после этого выбирайте default, локальную переменную, поле объекта или внешнее хранилище. Так вы не отклоните событие нового импорта из-за идентификатора, сохранённого предыдущим вызовом.
Ещё одна ловушка: два элемента ссылаются на один словарь
Даже без аргумента по умолчанию можно выдать несколько ссылок на одну изменяемую запись:
def aliases_bad():
row = {}
for n in (2, -1):
row['delta'] = n
yield row
items = list(aliases_bad())
print(items[0] is items[1]) # True
print([x['delta'] for x in items]) # [-1, -1]
Обе позиции списка видят последнее состояние row. Сам список не сделал снимок содержимого словаря при каждом yield. В нашей реализации каждое событие создаётся отдельно; сохранённые e1 и e2 имеют значения2 и-1 после дальнейшего чтения.
Новый dict на каждой итерации устранил бы именно это совместное верхнее состояние. Но поверхностное копирование не отделяет произвольные вложенные списки. Прежде чем предлагать deepcopy, выясните, нужны ли снимки, ссылки на живое состояние или неизменяемые значения и какова цена копирования.

Почему break не является договором о закрытии?
Выполненный сценарий сохранил ссылку на генератор, получил первое событие и вышел из for через break. Контролируемый источник остался открытым: reads=1, closed=false. Затем явный g.close() завершил контекст и установил closed=true. Мы специально не полагались на момент сборки мусора.
Официальное описание методов генератора задаёт поведение close. Документация contextlib.closing показывает обёртку, вызывающую close при выходе из контекста. Для потребителя нашего генератора можно записать:
from contextlib import closing
with closing(events(source)) as stream:
for event in stream:
consume(event)
break
consume здесь обозначает действие потребителя и не реализует запись в базу. В тесте после такого break источник закрыт и прочитана только одна строка. Обёртка closing управляет объектом генератора; его завершение выходит из внутреннего with, который управляет Source.
Есть ещё одна граница: close у ни разу не запущенного генератора не вошёл в Source. entered остался0; closed остался false, потому что ресурс в нашем контракте вообще не приобретался. Не требуйте отметку «закрыт» от того, что ещё не открывали. Если реальный ресурс приобретён до создания генератора, ответственность будет другой.
Исключение чтения и исключение закрытия
Неверный JSON, например одиночная {, оборачивается в ValueError с номером строки и исходным JSONDecodeError в __cause__. Такой подход соответствует официальному описанию цепочек исключений Python. Ошибка типа delta не является JSONDecodeError: это отдельная проверка уже разобранного значения.
В лаборатории дополнительно задан Source, который при выходе выбрасывает OSError. Когда неверная delta встречается вместе с ошибкой закрытия, наружу выходит OSError, а исходный ValueError сохраняется в __context__. Нельзя обещать, что наличие finally или with обязательно оставляет первоначальную ошибку главным исключением.
Наш Source устанавливает флаг closed до выброса OSError; это не отметка успешного закрытия реального ресурса. Поэтому он фиксирует попытку завершения нашего mock, а не подтверждает успешное освобождение реального дескриптора. При производственной диагностике нужно сохранять обе причины и определить политику журналирования, повторов и отчёта об ошибке закрытия.
Не перехватывайте любой Exception ради пустого списка, если это стирает различие между пустым импортом и неуспешным импортом. Решение о пропуске плохой строки допустимо лишь при явном контракте, который сохраняет номер, причину и счётчик отклонённых записей. Такой режим здесь не реализован.
Как проверить ответ без догадок
Сначала предскажите результат и состояние Source после каждого next, затем запустите сценарий. Отдельно проверьте исчерпание, повтор id, bool вместо int, некорректный JSON, два вызова с default и два сохранённых словаря. Эти случаи направлены на разные виды состояния; один зелёный happy path их не заменяет.
Все38 контролей локального пакета прошли в CPython3.12.14. Часть проверяет значения, часть — счётчики и цепочки исключений. Скрипт не измеряет память и не подтверждает безопасность параллельного потребления. Множество seen растёт с числом уникальных id: ленивое чтение не делает его автоматически ограниченным по памяти.
При следующем расширении выберите одно требование: ограниченная дедупликация по окну, хранение принятых событий, повтор после сбоя или обработка огромной строки. Для каждого потребуется собственный контракт. Для каждого обещания подготовьте пример, который должен сломать неверную реализацию.
Разбор ответа: четыре независимых критерия
Оцените свой ответ по наблюдаемым признакам, а не по числу названных терминов. Эта таблица — авторская самопроверка; она не является шкалой найма работодателя или предсказанием результата интервью.
| Критерий | Достаточное объяснение | Что остаётся пробелом |
|---|---|---|
| Потребление | Называете reads0 при создании и reads3 при ошибке третьей строки | Говорите «ленивый», но предполагаете чтение e4 |
| Состояние | Различаете seen внутри партии, default между вызовами и общий row | Исправляете только одну из двух причин общего состояния |
| Ошибка | Указываете уже выданные e1/e2, номер строки и цепочку причин | Обещаете автоматический откат предыдущих действий |
| Ресурс | Показываете break с сохранённой ссылкой и явный closing | Рассчитываете на неопределённый момент удаления объекта |
Если один критерий не выполнен, выберите соответствующий короткий сценарий и перескажите его по шагам. Например, для ресурса достаточно первой строки и break; неверная третья строка здесь только отвлечёт от причины. Для цепочки исключений, наоборот, нужен контролируемый сбой завершения. Подмена одного сценария другим даёт уверенный рассказ без проверки нужного свойства.
Полезный дополнительный вопрос: кто решает судьбу уже принятого события при повторном запуске импорта? Локальный seen не хранится между запусками и не доказывает идемпотентность внешней записи. Если это требуется, добавьте устойчивый ключ и согласуйте повторное применение эффекта. В текущих38 проверках нет такого хранилища, сбоя процесса или восстановления после него.
Пять заданий PracHub для продолжения
Задания доступны на английском. Объясните по-русски, какое состояние переносится между вызовами и какой объект отвечает за завершение, затем переходите к реализации.
| Полный заголовок задания | Что проверить |
|---|---|
| Implement lazy unique-merge generator for sorted streams | Не потреблять поток раньше необходимого и отдельно определить правило уникальности. |
| Compare Python Generators, Decorators, and Context Managers | Различить создание объекта, продвижение и владение закрытием. |
| Implement a robust Python generator | Указать поведение при ошибке и досрочном завершении. |
| Python and pytest Fundamentals: Fixtures, Decorators, and Generators | Составить наблюдаемые проверки cleanup вместо предположений. |
| Analyze and debug Python utilities | Найти общую изменяемую память и предсказать точные результаты. |
Выберите Implement a robust Python generator и защитите решение четырьмя фактами: когда начинается чтение, что уже выдано, где хранится состояние и кто закрывает ресурс. Это делает ответ проверяемым даже при изменении исходного задания.
Sources and Further Reading
- Python3.12: Iterators and Generators — протокол и приостановка выполнения.
- Python3.12: Default Argument Values — время создания default.
- Python3.12: Generator-iterator methods — close и завершение генератора.
- Python3.12: contextlib.closing — явное закрытие потребителем.
- Python3.12: Exception Chaining — причина и контекст исключения.
- Python3.12: Frozen dataclasses — ограничения присваивания полям.
Comments (0)