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.

Sources

Related topics