Итераторы и генераторы: вопросы для собеседования (часть 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` в контексте повторных проходов?
AIterable может возвращать новый iterator при каждом `iter(obj)`, поэтому по нему обычно можно проходить несколько раз; iterator хранит состояние и исчерпывается.
BIterable всегда одноразовый, а iterator можно перебирать бесконечно.
CIterator нельзя использовать в `for`, а iterable можно.
DРазницы нет: любой объект можно итерировать сколько угодно раз.
Ответ: Iterable — это источник итераторов, iterator — это «курсор» по данным.
Список, строка, словарь — обычно `iterable`: `iter(obj)` создаёт новый `iterator`, поэтому вы можете сделать два независимых `for`-прохода. `iterator` (например, результат `iter(list)` или generator) сам хранит позицию и после исчерпания не «перематывается». Если нужен повторный проход, создавайте новый `iterator` или материализуйте данные.
18В отладке вы сделали `pairs = zip(users, spends)`, затем `debug = list(pairs)`. После этого `d = dict(pairs)` оказался пустым. Почему?
AПотому что `zip()` возвращает iterator, и `list(pairs)` его полностью исчерпало.
BПотому что `dict()` не умеет принимать результат `zip()`.
CПотому что `zip()` сортирует данные и удаляет пары.
DПотому что нужно писать `pairs = zip(spends, users)`.
Ответ: `zip()` в Python 3 возвращает одноразовый итератор; любые операции, которые его «вычитывают», делают его пустым для повторного использования.
Объект из `zip(users, spends)` хранит позицию в потоке пар. Когда вы вызываете `list(pairs)`, он перебирается до конца (внутри через `next()`), и теперь в `pairs` уже нет элементов. Поэтому `dict(pairs)` получает пустой итератор и создаёт пустой словарь. Решение: материализовать один раз (`pairs_list = list(zip(...))`) или заново вызвать `zip(users, spends)` для второго прохода.
19Есть `xs = [1, 2, 3]`, `it = iter(xs)`, затем `it2 = iter(it)`. Какое утверждение верно?
A`it2` — это тот же iterator (или указывает на него), поэтому чтение из `it2` продвигает `it`.
B`it2` — новый независимый iterator, который начнёт снова с 1.
C`it2` — копия списка `xs`.
D`iter(it)` превращает iterator в список.
Ответ: Для итератора `iter(it)` возвращает его же, а не новый проход.
Общее правило: `iter(iterable)` создаёт итератор, а `iter(iterator)` возвращает тот же объект итератора. Поэтому `it` и `it2` разделяют состояние: вызов `next(it2)` забирает следующий элемент у `it` и наоборот. Это частый источник багов, если ожидали «второй независимый проход».
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))` нельзя использовать в цикле.
BПотому что `for` не умеет работать с generator expression.
CПотому что нужно заменить `for b in g` на `for b in range(...)`.
DПотому что `g` — один iterator: внутренний цикл потребляет тот же `g`, и он не стартует заново для каждого `a`; нужно использовать исходный `xs` или создавать новый генератор.
Ответ: Один и тот же итератор нельзя использовать как независимый источник в двух вложенных циклах.
Generator expression создаёт один итератор с внутренним состоянием. Когда внешний `for a in g` берёт первый элемент, внутренний `for b in g` продолжает чтение этого же итератора со следующей позиции и быстро исчерпывает его. Поэтому последующие значения `a` могут вообще не появиться. Для всех пар нужно использовать независимые источники, например `for a in xs: for b in xs: ...` или создавать новый генератор внутри внешнего цикла.
Хотите тренировать интерактивно?
В приложении — таймер, прогресс, стрики и 1700+ вопросов по всем темам.
Тренировать в Telegram