Итераторы и генераторы: вопросы для собеседования (часть 4)
yield, генераторные выражения, протокол итератора (__iter__, __next__) — продвинутый Python, который спрашивают у кандидатов на middle+. Генераторы экономят память при обработке больших данных. На собеседовании просят объяснить ленивое вычисление, написать генератор или показать разницу между списком и генератором.
Вопросы 16–20 из 20
16Вы хотите сделать два независимых прохода по одному и тому же результату фильтрации: например, посчитать `sum(valid)` и затем `max(valid)` без пересоздания источника. Какой подход чаще всего проще и надёжнее?
AОставить generator и выполнить `sum(valid)` и `max(valid)` подряд — generator сам переиспользуется.
BПеред каждым проходом вызывать `iter(valid)` — это «перемотает» generator.
CСначала вызвать `next(valid)` до конца, чтобы «подготовить» данные для второго прохода.
DМатериализовать результат в список один раз, например `valid_list = [x for x in xs if ...]`, и уже по нему считать метрики.
Ответ: Если нужен повторный проход без пересоздания источника, материализуйте данные; генератор одноразовый.
Generator expression и iterator в целом потребляются при первом проходе. Поэтому второй агрегат (например, `max(valid)`) увидит уже пустой поток. Если данные не слишком большие и вам нужен повторный доступ, проще один раз создать список и затем выполнять сколько угодно проходов. Альтернатива — каждый раз заново создавать генератор/итератор из исходного iterable.
Подробный разбор → 17Какое объяснение лучше всего описывает разницу между `iterable` и `iterator` в контексте повторных проходов?
A`iterable` всегда одноразовый, а `iterator` можно перебирать в `for` бесконечно сколько угодно раз без сброса состояния
B`iterator` нельзя использовать в `for`, а `iterable` можно: разница в том, какой объект поддерживает синтаксис цикла
C`iterable` возвращает новый `iterator` при каждом вызове `iter(obj)`, поэтому по нему обычно можно проходить несколько раз; `iterator` хранит состояние
DРазницы между `iterable` и `iterator` нет: любой объект в `Python` можно итерировать сколько угодно раз без потери данных и сброса позиции
Ответ: `iterable` — это источник итераторов, `iterator` — это «курсор» по данным, который исчерпывается после прохода.
Список, строка, словарь — обычно `iterable`: `iter(obj)` создаёт новый `iterator`, поэтому вы можете сделать два независимых `for`-прохода. `iterator` (например, результат `iter(list)` или генератор) сам хранит позицию и после исчерпания не «перематывается». Если нужен повторный проход, создавайте новый `iterator` или материализуйте данные.
Подробный разбор → 18В отладке вы сделали `pairs = zip(users, spends)`, затем `debug = list(pairs)`. После этого `d = dict(pairs)` оказался пустым словарём. Почему?
A`dict()` не принимает результат `zip()` без явного приведения к списку пар.
B`zip()` сортирует данные по первому аргументу и удаляет одинаковые ключи.
CНужно было писать `pairs = zip(spends, users)`, иначе ключи и значения меняются местами.
D`zip()` возвращает одноразовый итератор, и `list(pairs)` уже исчерпало его до конца.
Ответ: `zip()` возвращает одноразовый итератор: после первого полного обхода он становится пустым.
После `pairs = zip(users, spends)` объект `pairs` — это итератор, который выдаёт пары один раз. `debug = list(pairs)` уже прошёл его до конца, поэтому следующий `dict(pairs)` ничего не получает и возвращает пустой словарь. Чтобы такого не было, либо сразу собирают пары в список и переиспользуют его, либо создают новый `zip(...)` перед `dict(...)`. `zip` ничего не сортирует и не отбрасывает; порядок аргументов меняет лишь, что станет ключом, а что значением, но не объясняет пустоту словаря.
Подробный разбор → 19Есть `xs = [1, 2, 3]`, `it = iter(xs)`, затем `it2 = iter(it)`. Какое утверждение верно?
A`it2` ссылается на тот же итератор, что и `it`, и разделяет с ним состояние
B`it2` представляет независимый итератор от `iter(xs)` с обходом заново
C`it2` оказывается копией списка `xs`, эквивалентной вызову `list(it)`
D`it2` превращается в кортеж со всеми оставшимися элементами `xs`
Ответ: `iter()` от уже существующего итератора возвращает тот же объект, так что общее состояние сохраняется.
Итераторы в Python реализуют метод `__iter__`, возвращающий `self`. Поэтому `iter(it)` отдаёт тот же объект `it`, а не новый итератор и не копию данных. После `it2 = iter(it)` оба имени указывают на один и тот же итератор, и любой `next(it2)` сдвигает позицию, видимую и через `it`. Чтобы получить независимый обход, нужно сделать новый итератор, например `iter(xs)` ещё раз.
Подробный разбор → 20Хотите получить все пары значений из `xs = [1, 2, 3]`. Написали `g = (x for x in xs)` и затем `pairs = []; for a in g: for b in g: pairs.append((a, b))`. Почему `pairs` получится короче ожидаемого?
AВызов `pairs.append((a, b))` запрещён в теле вложенного `for` без явного предварительного создания списка нужного размера
BВнешний и внутренний цикл потребляют один итератор `g`: внутренний `for` дочитывает его до конца, и внешний остаётся без следующих элементов
CИмя `g` во внутреннем `for b in g` нужно заменить на `range(len(xs))`: иначе цикл не понимает по какому объекту проходить
DДля вложенных `for` обязателен `list`, а генераторное выражение возвращает объект без поддержки повторного обхода через `iter`
Ответ: Один и тот же итератор-генератор нельзя использовать как независимый источник в двух вложенных циклах.
Генераторное выражение создаёт один итератор с внутренним состоянием. Когда внешний `for a in g` берёт первый элемент, внутренний `for b in g` дочитывает тот же итератор до конца и исчерпывает его. Поэтому последующих значений `a` уже не будет — итератор не перезапускается. Чтобы получить все пары, используйте независимые источники: `for a in xs: for b in xs: ...` или создавайте новый генератор внутри внешнего цикла.
Подробный разбор →