List/dict/set comprehensions, generator expressions
Comprehensions — Pythonic способ построения коллекций из итерируемых источников за один проход. Синтаксически они ближе к математической нотации ({x² | x ∈ ℕ, x < 10}), чем традиционный for-loop с .append.
Ключевое различие, которое мы декомпозируем: list-comp аллоцирует все N элементов в памяти; generator expression — O(1) памяти, элементы produced лениво. Для N=10⁶ — это разница между 8MB и 120 байтами.
List comprehension
Базовая форма: [expr for x in iter if cond]. Эквивалентна:
result = []
for x in iter:
if cond:
result.append(expr)
# result == [expr for x in iter if cond]
Comprehension компактнее и обычно быстрее: bytecode для list-comp оптимизирован — не вызывает LOAD_METHOD + CALL для .append на каждой итерации, использует специальный opcode LIST_APPEND. См. dis.dis для разницы.
Примеры:
squares = [x**2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
evens = [x for x in range(20) if x % 2 == 0]
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# Nested comprehension — внешний for выполняется раньше:
matrix = [[i * j for j in range(3)] for i in range(3)]
# [[0, 0, 0], [0, 1, 2], [0, 2, 4]]
# Multiple for clauses (cartesian product, flat):
pairs = [(x, y) for x in [1, 2] for y in [3, 4]]
# [(1, 3), (1, 4), (2, 3), (2, 4)]
Dict / set comprehensions
Те же brackets, но с {}:
# Dict comprehension: {key_expr: value_expr for x in iter}
cubes = {x: x**3 for x in range(5)}
# {0: 0, 1: 1, 2: 8, 3: 27, 4: 64}
# Set comprehension: {expr for x in iter} — dedup автоматический
unique_squares = {x**2 for x in [-2, -1, 0, 1, 2]}
# {0, 1, 4} (4 уникальных значения, хотя на входе 5)
Dict comprehension — самый частый use case для построения lookup-таблиц из последовательностей. Напоминание из M02 урок 04: insertion order preserved в dict (PEP 468), так что порядок ключей в comprehension результата = порядок iteration.
Set comprehension — для dedup + filter за один проход. О(N) ожидаемое время благодаря O(1) avg вставке в hash table (см. M02 урок 03).
Comprehension scope (Python 3)
В Python 2 переменная цикла «утекала» наружу: [x for x in range(5)]; print(x) печатал 4. Python 3 это исправил: comprehension имеет dedicated scope, и переменная цикла не видна снаружи:
# Python 3:
[x for x in range(5)]
print(x) # NameError: name 'x' is not defined (если x не определён извне)
# Альтернативно:
x = 'outer'
[x for x in range(5)] # внутренний x — отдельный
print(x) # 'outer'
Comprehension в bytecode компилируется в отдельную функцию (см. dis.dis): создаётся frame, исполняется тело, возвращается результат. Это explains scope isolation — по той же механике, что обычные функции.
Generator expression
Парадигма: те же операторы, но () вместо [] / {}:
gen = (x**2 for x in range(10))
print(type(gen)) # <class 'generator'>
Это не создаёт list. Это создаёт generator — ленивый итератор, производящий элементы по требованию (по одному за вызов next(gen)).
gen = (x**2 for x in range(5))
print(next(gen)) # 0
print(next(gen)) # 1
print(next(gen)) # 4
print(list(gen)) # [9, 16] — оставшиеся два, после чего generator exhausted
print(list(gen)) # [] — повторно итерировать нельзя
Ключевое: generator single-pass. После исчерпания (StopIteration) повторно итерировать нельзя — нужно создать новый.
Memory profile: list-comp vs generator-expression
Это главная причина выбора между ними:
import sys
# list-comp: все N элементов аллоцированы
lst = [x for x in range(10**6)]
print(sys.getsizeof(lst)) # ~8 MB
# generator expression: только generator object
gen = (x for x in range(10**6))
print(sys.getsizeof(gen)) # ~120 байт (constant!)
list — это PyListObject + array of PyObject* для всех 10⁶ элементов (см. M02 урок 01). Generator — это PyGenObject + ссылка на frame; ничего из values не materialized.
Для агрегатных операций (sum, max, min, any, all) всегда используйте gen-expr:
# Хорошо — O(1) памяти, streaming:
total = sum(x**2 for x in range(10**6))
# Плохо — materializes 8MB list, потом sum:
total = sum([x**2 for x in range(10**6)])
Скобки sum’а делают gen-expr — [] не нужны.
When to use comprehension vs gen-expr
| Need | Choose | Why |
|---|---|---|
Aggregation (sum, max, any) | gen-expr | потребитель читает по одному, materialize всё — лишняя память |
| List, который обходится несколько раз | list-comp | gen-expr exhausted после first pass |
| Передача в функцию, ожидающую list (sorted, json.dumps) | list-comp | API contract |
| Pipeline transformations (chained map/filter) | gen-expr | streaming, O(1) память на этап |
Need len() или indexing | list-comp | gen-expr не поддерживает |
Walrus в comprehension — PEP 572
Python 3.8 добавил оператор := (walrus) — assignment expression. Позволяет capture intermediate value прямо в выражении:
# Без walrus — два прохода или явный list comprehension с .__getitem__:
import math
data = [-2, -1, 0, 1, 2, 3]
expensive = [math.sqrt(x) for x in data if x >= 0]
filtered = [y for y in expensive if y > 1.0]
# C walrus — single pass + capture:
filtered = [y for x in data if x >= 0 and (y := math.sqrt(x)) > 1.0]
Использование разумное: когда expr дорогой и нужен и в filter, и в результате. Не злоупотребляйте — читаемость выигрывает только для простых случаев.
См. PEP 572 — Assignment Expressions.
Anti-patterns
Comprehension со side effects — [print(x) for x in items] создаёт ненужный list [None, None, ...] и сбивает читателя с толку. Используйте обычный for-loop.
Nested comprehension >2 уровней — становится нечитаемым. Если есть три вложенных for, разбейте на функции или замените for-loop’ом.
list-comp где gen-expr достаточно — для агрегатов это wasted memory. Если результат сразу потребляется одной функцией (sum, max, any), используйте gen-expr.
Cross-course context
DataFrame API DataFusion: lazy evaluation и план запросаКлючевые выводы
- Comprehensions (list/dict/set) — компактный способ построения коллекций; bytecode-оптимизирован (LIST_APPEND opcode), быстрее обычного for-loop с .append.
- Generator expression —
()вместо[], ленивый, O(1) память, exhausted после first pass. - Memory profile критичен: list-comp materializes все N элементов; gen-expr хранит только iterator state. Для агрегатов всегда gen-expr.
- Walrus
:=(PEP 572) — capture intermediate в comprehension для single-pass filter+expr. - Comprehension scope в Python 3 isolated — переменная цикла не утекает в окружающий scope.