Списковые включения в Python: пять строк в одну без потери читаемости
Comprehension сжимает цикл с накоплением в одну строку. Но та же техника легко превращается в нечитаемое месиво. Разбираем, где выигрыш реальный, а где лучше оставить обычный for.

Вы пишете цикл: создаёте пустой список, проходите по данным, добавляете отфильтрованные элементы через append. Пять строк ради одной операции. Списковое включение (list comprehension) сжимает это до одной строки — и в CPython 3.12 работает быстрее ручного цикла, потому что не вызывает append на каждой итерации. Но у той же конструкции есть порог, за которым краткость оборачивается кодом, который никто не хочет ревьюить. Разберём, где включение выигрывает, чем платит и по какому критерию выбирать между ним и обычным for.
Всё ниже проверено на CPython 3.12. Синтаксис включений стабилен с Python 2.x, но детали производительности и поведение области видимости в примерах относятся к текущим 3.x.
Что такое comprehension и когда он к месту
Списковое включение — это выражение, которое строит список из итерируемого объекта за один проход. Базовая форма: [выражение for элемент in источник if условие]. Условие необязательно.
Сравните две записи одной задачи — квадраты чётных чисел от 0 до 9:
# Обычный цикл
squares = []
for n in range(10):
if n % 2 == 0:
squares.append(n * n)
# Списковое включение
squares = [n * n for n in range(10) if n % 2 == 0]
Оба дают [0, 4, 16, 36, 64]. Вторая версия короче и читается как одно предложение: «квадраты n для каждого чётного n из диапазона». Включение к месту, когда операция ровно такая — взять источник, отфильтровать, преобразовать и собрать в новый список.
Списковое включение хорошо ровно до тех пор, пока его можно прочитать вслух как одну фразу. Как только для понимания приходится водить пальцем по строке — это уже обычный цикл, замаскированный под однострочник.
Где включение выигрывает, а где проигрывает
У comprehension есть три преимущества и один порог, за которым они исчезают.
- Скорость. Включение не делает поиск атрибута
appendи его вызов на каждой итерации — байткод оптимизирован под построение списка. На больших источниках это заметно. - Локальность. Результат виден на одной строке, не нужно искать, где инициализирован список и что в него добавляется.
- Намерение. Форма сама говорит: «строим новый список». Обычный цикл может делать что угодно ещё.
Порог наступает, когда во включение начинают засовывать логику, ради которой оно не создавалось.
Красный флаг 1: вложенные тернарники в выражении
# Плохо читается
labels = ['high' if x > 100 else 'mid' if x > 50 else 'low' for x in values]
Здесь три уровня логики упакованы в выражение. Через месяц вы сами не сразу поймёте порядок проверок. Вынесите в функцию:
def label(x):
if x > 100:
return 'high'
if x > 50:
return 'mid'
return 'low'
labels = [label(x) for x in values]
Включение осталось читаемым, а логика — там, где её удобно тестировать.
Красный флаг 2: два и более for с фильтрами
# Двойная вложенность плюс условие — уже тяжело
pairs = [(i, j) for i in range(5) for j in range(5) if i != j if i + j > 4]
Читается слева направо, но порядок циклов неочевиден: первый for — внешний, второй — внутренний, как во вложенном цикле. Если тут появится третий for — переписывайте на обычные циклы.
Красный флаг 3: побочные эффекты
# Так делать нельзя
[print(x) for x in items] # включение ради побочного эффекта
Здесь строится и выбрасывается список из None. Включение существует, чтобы вернуть список. Если результат не нужен — пишите обычный for.
Сравнение форм построения коллекций
Списковое включение — не единственная форма. По той же схеме строятся словари, множества и генераторы. Выбор зависит от того, что вы делаете с результатом.
| Форма | Синтаксис | Когда выбирать |
|---|---|---|
| Списковое включение | [x for x in src] | Нужен список: индексация, повторный проход, срезы |
| Генераторное выражение | (x for x in src) | Проход один раз, источник большой, память важна |
| Словарное включение | {k: v for k, v in src} | Строите отображение ключ → значение |
| Включение множества | {x for x in src} | Нужны уникальные значения без порядка |
Ключевая развилка — список или генератор. Списковое включение материализует все элементы в памяти сразу. Генераторное выражение* отдаёт элементы по одному и не хранит весь результат.
# Список: 10 млн int держатся в памяти целиком
total = sum([n * n for n in range(10_000_000)])
# Генератор: элементы считаются по одному, память почти не растёт
total = sum(n * n for n in range(10_000_000))
Для sum, any, max и любого одноразового прохода генератор экономит память без потери скорости. Список нужен, только когда результат используется повторно или требуется индексация.
Когда что выбирать: критерий
Правило простое и не про вкус.
- Выражение читается как одна фраза, один
for, максимум одинif— списковое включение. - Результат нужен один раз и источник большой — генераторное выражение, замените квадратные скобки на круглые.
- Логики больше одного условия или тернарник вложенный — вынесите преобразование в функцию, оставьте включение однострочным.
- Есть побочный эффект, два и более
for, или нужно поймать исключение внутри — обычный циклfor. Читаемость важнее строки.
Что ломается на практике
Три ошибки встречаются чаще всего.
Обработка исключений. Внутри включения нет места для try/except. Если преобразование может упасть — int(x) на нечисловой строке даст ValueError — включение развалится целиком, и вы не узнаете, на каком элементе. Выносите преобразование в функцию с try/except внутри или пишите обычный цикл.
Утечка имени в старом коде. В Python 3.x переменная цикла живёт в своей области видимости и не протекает наружу — после [x for x in range(3)] имени x в окружающем коде не будет. В Python 2 она протекала. Если переносите код с Python 2, не рассчитывайте на переменную включения снаружи.
Генератор исчерпывается. Генераторное выражение проходится один раз. Второй sum по тому же генератору вернёт 0:
gen = (n for n in range(5))
print(sum(gen)) # 10
print(sum(gen)) # 0 — генератор уже исчерпан
Если результат нужен дважды — берите список.
Что делать дальше
Пройдитесь по своему коду и найдите циклы вида «создать пустой список — for — append». Именно они переписываются во включение без потери читаемости. Циклы с try/except, с двумя вложенными for и с побочными эффектами не трогайте.
Дальше стоит разобрать генераторные выражения под нагрузкой — где именно ленивое вычисление экономит память в конвейере обработки данных — это отдельная тема. И заглянуть в модуль itertools: часть задач, которые тянет решить многоэтажным включением, там уже решена читаемыми функциями вроде chain и filterfalse.
* Генераторное выражение — конструкция, которая вычисляет элементы по требованию, а не строит весь результат сразу. Отличается от спискового включения только круглыми скобками вместо квадратных.
Субтитры и таймкоды: Расшифровка речи и субтитры по правилам читаемости: 42 знака в строке, две строки, минимум секунда на реплику.
Спросить за 40 ₽Источники: Python documentation — List Comprehensions (The Python Tutorial), Python documentation — Generator expressions (Expressions reference)
Частые вопросы
А если у меня Python 3.8 или старее — примеры сработают?
Списковое включение всегда быстрее обычного цикла?
Что выбрать, если не сработало и включение падает с ошибкой на одном элементе?
Когда точно не стоит использовать comprehension?
Чем генераторное выражение отличается от спискового включения на проде?
Протекает ли переменная цикла из включения наружу?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.