Чем генераторное выражение (x for x in ...) отличается от list comprehension?
Вижу в коде два почти одинаковых варианта и не пойму, в чём разница, кроме скобок:
a = [x * x for x in range(1_000_000)] # квадратные скобки
b = (x * x for x in range(1_000_000)) # круглые скобки
Печать b выдаёт что-то типа <generator object ...>, а не список. Когда какой брать? Это про память?
2 ответа
Да, разница по сути одна, но важная: ленивость и память.
[...](list comprehension) сразу строит весь список в памяти — миллион элементов лежат целиком.(...)(генераторное выражение) ничего не вычисляет заранее. Это итератор, который выдаёт элементы по одному, по запросу. В памяти он почти ничего не занимает, пока ты его не крутишь.
import sys
a = [x * x for x in range(1_000_000)]
b = (x * x for x in range(1_000_000))
print(sys.getsizeof(a)) # несколько мегабайт
print(sys.getsizeof(b)) # ~200 байт — сам объект-генератор, элементов ещё нет
Когда что брать:
- Нужен список целиком (индексация
a[5], повторный проход,len()) → list comprehension. - Надо просто пройтись один раз, особенно по большому/бесконечному источнику, или сразу свернуть в
sum/max/any→ генераторное выражение, сэкономишь память.
Отдельный приятный момент: если генераторное выражение — единственный аргумент функции, скобки можно не дублировать:
total = sum(x * x for x in range(1_000_000)) # без лишних скобок, лениво
Главная ловушка генератора: он одноразовый. Прошёлся по нему один раз — он опустел, второй раз for по нему ничего не даст. Список так можно гонять сколько угодно.
Добавлю быстрый ориентир «когда генератор не нужен»: на маленьких коллекциях list comprehension часто даже чуть быстрее и удобнее (можно индексировать, отлаживать). Ленивость окупается на больших данных или когда ты прерываешь обход раньше конца (next(), break, any(...)) — тогда генератор не считает лишнего.
И помни про одноразовость: если случайно сохранил генератор в переменную и прошёлся по нему дважды — на второй раз получишь пусто, без ошибки. Очень частый баг.