Генераторные выражения в Python

Генераторные выражения в Python: синтаксис, ленивые вычисления, экономия памяти, условия и сравнение со списковыми включениями.

Генераторное выражение — это компактный способ создать объект-генератор. Синтаксически похоже на списковое включение, но использует круглые скобки () вместо квадратных [] и вычисляет элементы лениво — только по запросу.

Синтаксис

# Списковое включение — вычисляет ВСЁ сразу, хранит в памяти
squares_list = [n ** 2 for n in range(5)]

# Генераторное выражение — вычисляет по одному, только когда просят
squares_gen = (n ** 2 for n in range(5))

print(squares_list)           # [0, 1, 4, 9, 16]
print(squares_gen)            # <generator object ...>
print(list(squares_gen))      # [0, 1, 4, 9, 16]

Вывод:

[0, 1, 4, 9, 16]
<generator object <genexpr> at 0x...>
[0, 1, 4, 9, 16]

Перебор элементов

evens = (n for n in range(10) if n % 2 == 0)

for e in evens:
    print(e, end=" ")

Вывод:

0 2 4 6 8 

Ленивые вычисления и экономия памяти

Главное преимущество: генераторное выражение не хранит все элементы в памяти — оно вычисляет следующий элемент только тогда, когда его запрашивают.

import sys

n = 1_000_000

list_comp  = [x ** 2 for x in range(n)]
gen_expr   = (x ** 2 for x in range(n))

print(f"Список:    {sys.getsizeof(list_comp):,} байт")
print(f"Генератор: {sys.getsizeof(gen_expr):,} байт")

Вывод (приблизительно):

Список:    8,448,728 байт
Генератор: 104 байт

Генератор занимает ~100 байт независимо от числа элементов — он не хранит коллекцию, только логику вычисления следующего элемента.

Генераторное выражение vs генераторная функция

Одну и ту же логику можно записать двумя способами:

# Генераторная функция (с yield)
def squares_fn(n):
    for i in range(n):
        yield i ** 2

# Генераторное выражение
squares_ge = (i ** 2 for i in range(5))

# Результат одинаковый
print(list(squares_fn(5)))   # [0, 1, 4, 9, 16]
print(list(squares_ge))      # [0, 1, 4, 9, 16]

Вывод:

[0, 1, 4, 9, 16]
[0, 1, 4, 9, 16]

Генераторное выражение — это синтаксический сахар для простых генераторных функций без состояния.

С условием и вложенным циклом

# Только нечётные квадраты
odd_squares = (n ** 2 for n in range(10) if n % 2 != 0)
print(list(odd_squares))

Вывод:

[1, 9, 25, 49, 81]
# Декартово произведение (вложенный цикл)
pairs = ((x, y) for x in range(1, 3) for y in range(1, 3))
print(list(pairs))

Вывод:

[(1, 1), (1, 2), (2, 1), (2, 2)]

Передача в функцию без лишних скобок

Когда генераторное выражение — единственный аргумент функции, внешние скобки можно опустить:

numbers = [3, 1, 4, 1, 5, 9, 2, 6]

total  = sum(n ** 2 for n in numbers)
maxi   = max(n * 2 for n in numbers)
joined = ", ".join(str(n) for n in numbers)

print(total)
print(maxi)
print(joined)

Вывод:

179
18
3, 1, 4, 1, 5, 9, 2, 6

Генератор исчерпывается

gen = (n for n in range(3))

print(list(gen))   # [0, 1, 2]
print(list(gen))   # [] — уже исчерпан!

# Создаём новый
gen = (n for n in range(3))
print(list(gen))   # [0, 1, 2]

Вывод:

[0, 1, 2]
[]
[0, 1, 2]

Сравнение: список, генераторное выражение, генераторная функция

КритерийСписковое включениеГенераторное выражениеГенераторная функция
Синтаксис[...](...)def + yield
Возвращаетсписокобъект-генераторобъект-генератор
Памятьвсё сразупо одному элементупо одному элементу
Повторный переборданет (исчерпывается)нет (исчерпывается)
Сложная логиканетнетда (несколько yield, try/except)

Коротко

  • Генераторное выражение — (...) — создаёт объект-генератор с ленивыми вычислениями.
  • Экономит память: хранит логику, а не коллекцию — ~100 байт независимо от размера.
  • Поддерживает фильтрацию (if) и вложенные циклы — как списковые включения.
  • При передаче в функцию как единственный аргумент внешние скобки не нужны.
  • Исчерпывается после однократного перебора — создавайте новый экземпляр для повтора.
Проверьте себя
1. Чем генераторное выражение синтаксически отличается от спискового включения?
AГенераторное выражение использует фигурные скобки {}
BГенераторное выражение использует круглые скобки (), списковое включение — квадратные []
CВ генераторном выражении нельзя использовать условие if
DРазницы в синтаксисе нет
2. Что произойдёт при повторном запуске for square in squares после того, как генераторное выражение squares = (n**2 for n in range(5)) уже было полностью перебрано?
AПеребор начнётся сначала
BНичего не выведется — генератор исчерпан
CВызовется исключение StopIteration
DВыведется первый элемент и остановится
3. Списковое включение и генераторное выражение создают квадраты от 0 до 999999. Какое из утверждений верно?
AОба занимают одинаковое количество памяти
BГенераторное выражение занимает больше памяти, так как хранит состояние
CСписковое включение быстрее из-за оптимизации Python
DСписковое включение создаёт все элементы сразу и загружает их в память; генераторное выражение — по одному по запросу

Закрепите практикой

Задачи с автоматической проверкой — решайте прямо здесь, не уходя из учебника.

py
Курс по теме
Пройдите курс «Python с нуля» — по шагам, с проверкой
8 уроков · ~14 ч · теория, упражнения и экзамен с бейджем
Открыть курс →