ШПАРГАЛКА

Comprehensions и генераторы Python

Шпаргалка по comprehensions и генераторам Python: list/dict/set, генераторные выражения, yield, ленивые вычисления, itertools и частые ошибки.

Comprehensions — это компактный синтаксис Python для построения коллекций из итерируемых объектов. Генераторы решают ту же задачу, но лениво: выдают элементы по одному и не держат весь результат в памяти. В этой шпаргалке — list/dict/set comprehensions, генераторные выражения, ключевое слово yield, экономия памяти, связка с itertools и типичные ловушки.

List comprehension: основы

List comprehension собирает список в одну строку. Базовый синтаксис: [выражение for элемент in итерируемое]. Сравним обычный цикл и comprehension — результат одинаковый, но второй вариант короче и читается как единое целое.

# Обычный цикл
squares = []
for x in range(5):
    squares.append(x ** 2)
print(squares)  # [0, 1, 4, 9, 16]

# То же через list comprehension
squares = [x ** 2 for x in range(5)]
print(squares)  # [0, 1, 4, 9, 16]

Когда использовать. Comprehension хорош для простого преобразования или фильтрации. Если внутри сложная логика, несколько if/else вперемешку или побочные эффекты — лучше оставить обычный цикл, так читабельнее.

List comprehension с условием

Условие if в конце работает как фильтр: в результат попадают только подходящие элементы.

# Фильтрация: только чётные
evens = [x for x in range(10) if x % 2 == 0]
print(evens)  # [0, 2, 4, 6, 8]

# Два условия подряд (логическое И)
nums = [x for x in range(30) if x % 2 == 0 if x % 3 == 0]
print(nums)  # [0, 6, 12, 18, 24]

Если нужно выбрать значение, а не отфильтровать, используйте тернарный if/else — он стоит перед for и относится к выражению, а не к фильтру.

# Тернарный if/else: преобразуем, ничего не выбрасывая
labels = ["чёт" if x % 2 == 0 else "нечёт" for x in range(5)]
print(labels)  # ['чёт', 'нечёт', 'чёт', 'нечёт', 'чёт']

Запомните разницу:

Где стоит ifРольПример
После forФильтр (без else)[x for x in xs if x > 0]
Перед forВыбор значения (с else)[x if x > 0 else 0 for x in xs]

Вложенные comprehensions

Несколько for подряд читаются слева направо, как вложенные циклы. Это удобно, чтобы развернуть матрицу или перебрать пары.

# Развернуть список списков (flatten)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in matrix for x in row]
print(flat)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Эквивалент обычными циклами
flat = []
for row in matrix:      # первый for
    for x in row:       # второй for
        flat.append(x)

Чтобы получить вложенный список (список списков), comprehension помещают внутрь другого comprehension.

# Транспонирование матрицы
matrix = [[1, 2, 3], [4, 5, 6]]
transposed = [[row[i] for row in matrix] for i in range(3)]
print(transposed)  # [[1, 4], [2, 5], [3, 6]]

Совет. Глубже двух уровней вложенности читать тяжело — разбейте на именованные циклы.

Dict comprehension

Строит словарь: синтаксис {ключ: значение for ...}. Удобно для инверсии, фильтрации и преобразования словарей.

# Словарь квадратов
squares = {x: x ** 2 for x in range(5)}
print(squares)  # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# Поменять местами ключи и значения
d = {"a": 1, "b": 2}
inverted = {v: k for k, v in d.items()}
print(inverted)  # {1: 'a', 2: 'b'}

# Отфильтровать словарь по значению
prices = {"хлеб": 50, "молоко": 80, "икра": 900}
cheap = {k: v for k, v in prices.items() if v < 100}
print(cheap)  # {'хлеб': 50, 'молоко': 80}

Внимание: если ключ повторится, в словаре останется последнее значение — дубликаты молча перетираются.

Set comprehension

Строит множество в фигурных скобках без двоеточия: {выражение for ...}. Автоматически убирает дубликаты.

# Уникальные длины слов
words = ["кот", "пёс", "слон", "ёж"]
lengths = {len(w) for w in words}
print(lengths)  # {1, 3, 4}  (порядок не гарантирован)

# Уникальные остатки
remainders = {x % 3 for x in range(10)}
print(remainders)  # {0, 1, 2}

Не перепутайте: {} — это пустой словарь, а не множество. Для пустого множества используйте set().

Генераторные выражения (generator expressions)

Выглядят как list comprehension, но в круглых скобках. Возвращают не список, а генератор — объект, который выдаёт элементы по одному и не хранит их все сразу.

# Круглые скобки -> генератор, а не список
gen = (x ** 2 for x in range(5))
print(gen)        # 
print(next(gen))  # 0
print(next(gen))  # 1
print(list(gen))  # [4, 9, 16]  (0 и 1 уже выданы)

Если генератор передаётся в функцию единственным аргументом, внешние скобки можно опустить.

# Лишние скобки не нужны
total = sum(x ** 2 for x in range(1000))
print(total)  # 332833500

Чем генератор отличается от списка

СвойствоList comprehension [...]Generator expression (...)
ВычислениеСразу всё (жадно)По требованию (лениво)
ПамятьХранит все элементыОдин элемент за раз
Повторный проходМожно много разОдноразовый
len(), индексПоддерживаютсяНет
# Генератор одноразовый: второй раз пусто
gen = (x for x in range(3))
print(list(gen))  # [0, 1, 2]
print(list(gen))  # []  -- генератор исчерпан

yield и функции-генераторы

Функция с yield вместо return становится генератором. При вызове она не выполняется сразу: тело запускается порциями, замирая на каждом yield и запоминая своё состояние.

def countdown(n):
    while n > 0:
        yield n      # отдать значение и "заморозить" функцию
        n -= 1

for x in countdown(3):
    print(x)  # 3, затем 2, затем 1

print(list(countdown(3)))  # [3, 2, 1]

Генераторы позволяют описывать бесконечные последовательности — главное не оборачивать их в list() целиком.

def naturals():
    n = 1
    while True:        # бесконечно
        yield n
        n += 1

gen = naturals()
print([next(gen) for _ in range(5)])  # [1, 2, 3, 4, 5]

yield from делегирует выдачу другому итерируемому — удобно для вложенных генераторов.

def chain(*iterables):
    for it in iterables:
        yield from it   # отдать все элементы it по очереди

print(list(chain([1, 2], [3, 4])))  # [1, 2, 3, 4]

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

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

import sys

# Список реально держит 10 млн чисел
lst = [x for x in range(10_000_000)]
print(sys.getsizeof(lst))  # ~ 80 МБ

# Генератор хранит только своё состояние
gen = (x for x in range(10_000_000))
print(sys.getsizeof(gen))  # ~ 200 байт

Ленивость хорошо сочетается с ранним выходом: если ответ нашёлся в начале, остальное даже не вычисляется.

# any() остановится на первом True и не пройдёт весь диапазон
has_big = any(x > 100 for x in range(1_000_000))
print(has_big)  # True (проверено всего 102 элемента)

# Построчное чтение огромного файла без загрузки целиком
def count_errors(path):
    with open(path) as f:
        return sum(1 for line in f if "ERROR" in line)

Связка с itertools

Модуль itertools даёт ленивые "кирпичики", которые отлично стыкуются с генераторами: вместе они обрабатывают потоки данных, не материализуя их.

from itertools import islice, count, chain, takewhile

# islice -- срез по генератору (у генератора нет [a:b])
first_5 = list(islice((x ** 2 for x in count()), 5))
print(first_5)  # [0, 1, 4, 9, 16]

# count() -- бесконечный счётчик, takewhile -- брать пока условие True
under_20 = list(takewhile(lambda x: x < 20, (x ** 2 for x in count())))
print(under_20)  # [0, 1, 4, 9, 16]

# chain -- склеить несколько источников лениво
merged = list(chain([1, 2], (x for x in range(3, 5))))
print(merged)  # [1, 2, 3, 4]

Подробнее об инструментах itertools смотрите в отдельной шпаргалке (если она доступна).

Типичные ошибки

1. Повторный проход по исчерпанному генератору. Генератор одноразовый — после list() или цикла он пуст.

gen = (x for x in range(3))
doubled = [x * 2 for x in gen]   # сработает
tripled = [x * 3 for x in gen]   # уже пусто!
print(doubled, tripled)  # [0, 2, 4] []

2. Утечка переменной (Python 2 vs 3). В Python 3 переменная цикла comprehension живёт в своей области и не протекает наружу — на это не стоит полагаться.

data = [i for i in range(5)]
# print(i)  # NameError в Python 3: i не существует снаружи

3. Поздняя привязка в генераторе с замыканием. Генератор ленив, поэтому внешние переменные считываются в момент исполнения, а не создания.

n = 10
gen = (x * n for x in range(3))
n = 100                 # меняем n ДО прохода
print(list(gen))        # [0, 100, 200] -- взялось новое n!

4. Лишняя память из-за списка вместо генератора. В sum(), any(), max() список строить незачем.

total = sum([x ** 2 for x in range(10**6)])  # строит список -> память
total = sum(x ** 2 for x in range(10**6))    # лениво -> лучше

5. Слишком сложный comprehension. Если в одну строку втиснуты несколько for и if, читать невозможно — разверните в обычный цикл. Краткость не стоит потери ясности.

# Нечитаемо
res = [f(x, y) for x in xs if g(x) for y in ys if h(x, y) if y > 0]

# Лучше -- обычные циклы с понятными именами
res = []
for x in xs:
    if not g(x):
        continue
    for y in ys:
        if h(x, y) and y > 0:
            res.append(f(x, y))

Шпаргалка-итог

Что нужноСинтаксис
Список[выр for x in it]
С фильтром[выр for x in it if усл]
Словарь{k: v for ... }
Множество{выр for ... }
Генератор(выр for ... )
Функция-генераторdef f(): yield ...

Правило выбора: нужен результат целиком и повторные проходы — берите []; обрабатываете большой/бесконечный поток или нужен один проход — берите (...) или функцию с yield.

Поддержать проект