Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 04.03 · 22 мин
Средний
ComprehensionGenerator expressionLazy evaluationPEP 572Memory profile
Требуемые знания:

List/dict/set comprehensions, generator expressions

Comprehensions — Pythonic способ построения коллекций из итерируемых источников за один проход. Синтаксически они ближе к математической нотации ({x² | x ∈ ℕ, x < 10}), чем традиционный for-loop с .append.

Ключевое различие, которое мы декомпозируем: list-comp аллоцирует все N элементов в памяти; generator expressionO(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.

Memory: list-comp vs generator-expression (N=10⁶)
List-compO(N) памятиВсе N элементов живут в памяти одновременно. PyListObject header + array PyObject* размером 10⁶ × 8 байт указателей + сами PyLong objects (~28 байт каждый, кешируется для small int [-5..256]). Для N=10⁶ это ~8MB.
vs
Gen-exprO(1) памятиGenerator object — компактный (~120 байт), содержит ссылку на frame с локальными переменными iterator. Элементы не materialized; produced лениво по next(). Подходит для streaming pipelines.

Для агрегатных операций (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

NeedChooseWhy
Aggregation (sum, max, any)gen-exprпотребитель читает по одному, materialize всё — лишняя память
List, который обходится несколько разlist-compgen-expr exhausted после first pass
Передача в функцию, ожидающую list (sorted, json.dumps)list-compAPI contract
Pipeline transformations (chained map/filter)gen-exprstreaming, O(1) память на этап
Need len() или indexinglist-compgen-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

WARNING

Comprehension со side effects[print(x) for x in items] создаёт ненужный list [None, None, ...] и сбивает читателя с толку. Используйте обычный for-loop.

WARNING

Nested comprehension >2 уровней — становится нечитаемым. Если есть три вложенных for, разбейте на функции или замените for-loop’ом.

WARNING

list-comp где gen-expr достаточно — для агрегатов это wasted memory. Если результат сразу потребляется одной функцией (sum, max, any), используйте gen-expr.


Cross-course context

DataFrame API DataFusion: lazy evaluation и план запроса

Ключевые выводы

  1. Comprehensions (list/dict/set) — компактный способ построения коллекций; bytecode-оптимизирован (LIST_APPEND opcode), быстрее обычного for-loop с .append.
  2. Generator expression() вместо [], ленивый, O(1) память, exhausted после first pass.
  3. Memory profile критичен: list-comp materializes все N элементов; gen-expr хранит только iterator state. Для агрегатов всегда gen-expr.
  4. Walrus := (PEP 572) — capture intermediate в comprehension для single-pass filter+expr.
  5. Comprehension scope в Python 3 isolated — переменная цикла не утекает в окружающий scope.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6