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.