PythonJuniorTechnical
Чем generator expression отличается от list comprehension?
List comprehension [x for x in src] сразу строит list в памяти. Generator expression (x for x in src) — ленивый iterator, выдаёт по элементу через next(). Generator экономит память и удобен для one-pass обработки; list нужен, если требуются индексация, len(), повторные проходы или сортировка.
Синтаксис и тип результата
data = range(1_000_000)
lst = [x * x for x in data] # list, ~8 MB, построен сразу
gen = (x * x for x in data) # generator, ~200 байт, лениво
print(type(lst)) # <class 'list'>
print(type(gen)) # <class 'generator'>
Аналогично есть {...} (set comprehension) и {k: v ...} (dict comprehension), они тоже жадные.
Когда брать generator expression
- One-pass агрегация:
sum(x*x for x in data),max(...),any(...),min(...). Промежуточный список не нужен. - Stream-обработка больших файлов:
(line.strip() for line in open(path)). - Pipeline-цепочки: фильтр + map без материализации каждого шага.
- Аргумент функции — Python разрешает опустить внешние скобки:
sum(x*x for x in data), неsum((x*x for x in data)).
Когда брать list comprehension
- Нужна индексация,
len(), срезы, повторные проходы. - Результат маленький, и удобство важнее памяти.
- Передаёте в API, которое ожидает sequence (например,
random.sample). - Многократная итерация в шаблонах (Jinja, Django) — generator при втором проходе пустой.
Подводный случай — одноразовость
gen = (x * x for x in range(5))
print(sum(gen)) # 30
print(sum(gen)) # 0 — генератор уже потреблён
print(list(gen)) # []
List такого поведения не показывает.
Производительность
- Память: generator — постоянная (frame), list — пропорциональная числу элементов.
- Время: для полного прохода list comprehension чуть быстрее (нет накладных расходов на каждый
next()). Generator выигрывает, когда работа прерывается раньше:any(condition for x in big)выйдет на первомTrue. - Создание list тяжёлое для очень больших источников — может вызвать MemoryError.
Замыкание переменной цикла
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs]) # [2, 2, 2] — все ссылаются на одно i
# Чтобы захватить значение:
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs]) # [0, 1, 2]
Это касается обоих типов comprehension.
Подводные камни
- Использовать generator там, где нужен повторный проход — второй раз он пустой.
- Индексация
gen[2]—TypeError: 'generator' object is not subscriptable. - Передать generator в API, который под капотом вызывает
len()— например,random.sample. - Запустить тяжёлое вычисление и сохранить generator «на потом» — оно отработает при первом потребителе, а не там, где выражение записано.
- Generator expression внутри функции с локальной переменной — переменная захватывается по имени, как в обычных closures.
- Print generator в логах —
<generator object <genexpr> at 0x...>вместо данных; нуженlist()илиrepr()с осторожностью.
Common mistakes
- Не знать про одноразовость generator.
- Говорить, что generator всегда быстрее.
- Путать скобки вокруг аргумента функции с tuple.
What the interviewer is testing
- Выбирает по памяти и повторному использованию.
- Понимает lazy evaluation.
- Может объяснить consumed generator.