К этому моменту вы уже умеете создавать список пустым и заполнять его в цикле: завести result = [], пройтись for по исходным данным и на каждом шаге дописывать элемент через append(). Это рабочий способ, но для простых случаев — взять что-то из одной коллекции, чуть преобразовать и сложить в другую — он получается длинным: три-четыре строки ради одной идеи. То же самое с словарями: завести пустой {} и заполнять его в цикле по ключу.
В Python для таких повторяющихся сценариев есть отдельный компактный синтаксис. Он не даёт ничего принципиально нового — под капотом всё тот же перебор и то же построение коллекции — но код становится короче и, если не перебарщивать, куда легче читается. Опытные разработчики используют эту запись постоянно, так что разбирать чужой код без неё будет неудобно.
Список одной строкой
Простыми словами: вы говорите Python сразу три вещи в одной фразе — откуда брать элементы, что с каждым из них сделать и куда сложить результат. Никакого промежуточного пустого списка заводить не нужно, Python сам собирает готовый список по ходу перебора.
Официально это называется генератор списка, по-английски list comprehension. Термин стоит запомнить: именно так эта конструкция называется в документации Python и в любой статье про его особенности.
Базовая форма выглядит так:
[выражение for элемент in коллекция]
Разберём пример:
numbers = [1, 2, 3, 4, 5]
squares = [n ** 2 for n in numbers]
print(squares)
# [1, 4, 9, 16, 25]
| Часть кода | Что происходит |
|---|---|
[ ... ] | Квадратные скобки говорят: результатом будет список. |
n ** 2 | Выражение — то, что кладём в новый список. Стоит первым, потому что описывает результат. |
for n in numbers | Обычный перебор — такой же, как в цикле for: n по очереди становится каждым элементом numbers. |
squares = ... | Готовый список сразу присваивается переменной — отдельный append() не нужен. |
Python выполняет это как фразу «дай мне n ** 2 для каждого n из numbers»: берёт 1, считает 1 ** 2 = 1, кладёт в список; берёт 2, считает 2 ** 2 = 4, кладёт следом; и так до конца numbers. Результат — список [1, 4, 9, 16, 25], ровно из пяти элементов, потому что в numbers было пять чисел.
Это полный аналог такого цикла: squares = [], затем for n in numbers: squares.append(n ** 2). Генератор списка — не новая возможность, а короткая запись уже знакомого действия.
Добавляем отбор: условие после for
Простыми словами: иногда нужен не весь список целиком, а только часть — например, только чётные числа или только длинные слова. Тогда в генератор списка добавляют проверку «пропускать этот элемент в результат или нет».
Официально это тоже часть list comprehension, только с условием — полная форма записывается так: [выражение for элемент in коллекция if условие]. Условие после for здесь работает как фильтр: элемент попадает в результат, только если условие для него истинно, а если ложно — элемент просто пропускается, без ошибок и без записи в список.
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = [n for n in numbers if n % 2 == 0]
print(evens)
# [2, 4, 6, 8, 10]
Разбор: выражение здесь — просто n, то есть элемент кладётся в результат без изменений. Условие if n % 2 == 0 проверяет остаток от деления на 2 для каждого n: если остаток 0, число чётное и попадает в evens; если остаток 1 — число пропускается. Из десяти чисел прошли пять, поэтому результат короче исходного списка.
Не путайте фильтр if в конце с тернарным оператором X if условие else Y, который вы уже видели раньше — в тернарном операторе if стоит внутри выражения, до for, и заменяет одно значение на другое, а не убирает элемент из результата.
Сравните: фильтр уменьшает список, тернарный оператор в выражении — нет.
numbers = [1, 2, 3, 4, 5]
labeled = ["чётное" if n % 2 == 0 else str(n) for n in numbers]
print(labeled)
# ['1', 'чётное', '3', 'чётное', '5'] -- элементов по-прежнему 5
Здесь всё выражение — это "чётное" if n % 2 == 0 else str(n): для каждого n Python сначала решает, чётное оно или нет, и подставляет либо строку "чётное", либо число, превращённое в строку функцией str(). Ни один элемент не выброшен — в результате по-прежнему пять значений, просто часть из них заменена.
Список списков: разворачиваем вложенные данные
Простыми словами: если у вас список, где каждый элемент — тоже список (например, несколько групп чисел или таблица), иногда нужно «разложить» всё это в один плоский список без вложенности. Для этого в генератор списка ставят подряд два for: один — чтобы взять очередной вложенный список, второй — чтобы взять из него очередной элемент.
Официально это называется вложенный генератор списка, а результат такого разворачивания коллекций обычно называют плоским списком (в противовес «вложенному»).
groups = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
flat = [x for group in groups for x in group]
print(flat)
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
| Часть кода | Что происходит |
|---|---|
for group in groups | Внешний перебор: group по очереди становится каждым вложенным списком — сначала [1, 2, 3], потом [4, 5], потом [6, 7, 8, 9]. |
for x in group | Внутренний перебор: для каждого group отдельно достаёт каждый его элемент x. |
x (выражение) | Каждый найденный x кладётся в итоговый список flat без изменений. |
Порядок двух for читайте слева направо, как во вложенном цикле: сначала внешний («для каждой группы»), потом внутренний («для каждого числа в этой группе»). Поменять их местами нельзя — Python сообщит об ошибке, потому что на первом шаге переменной x ещё неоткуда взяться.
Тот же приём для словаря
Простыми словами: та же самая идея — перебрать и сразу собрать результат — работает и для словаря. Разница только в том, что для каждого элемента нужно указать не одно значение, а пару: ключ и то, что за ним стоит.
Официально это называется генератор словаря, по-английски dict comprehension. Записывается в фигурных скобках, а вместо одного выражения указывается пара через двоеточие: {ключ: значение for элемент in коллекция}.
words = ["python", "list", "код"]
word_lengths = {w: len(w) for w in words}
print(word_lengths)
# {'python': 6, 'list': 4, 'код': 3}
Здесь w — очередное слово из words, оно же становится ключом нового словаря. Слева от двоеточия — ключ (w), справа — значение (len(w), длина этого слова). Python проходит по всем трём словам и для каждого добавляет в word_lengths пару «слово — его длина».
Ключом и значением может быть что угодно, в том числе результат вызова метода. Например, можно поменять местами ключи и значения уже существующего словаря:
prices = {"яблоко": 50, "арбуз": 200, "лимон": 80}
expensive = {name: price for name, price in prices.items() if price > 60}
print(expensive)
# {'арбуз': 200, 'лимон': 80}
prices.items() отдаёт пары «ключ, значение», поэтому перебор сразу распаковывается в две переменные — name и price — так же, как вы уже делали при переборе словаря циклом for. Условие if price > 60 работает как фильтр, знакомый по спискам: в новый словарь попадают только те пары, где цена больше 60.
И для множества
Простыми словами: если нужен не список и не словарь, а набор уникальных значений без повторов — тот же приём работает и для множества, просто повторяющиеся результаты Python уберёт сам, как он всегда делает с множеством.
Официально это генератор множества, set comprehension: фигурные скобки без двоеточия, как у обычного множества — {выражение for элемент in коллекция}.
words = ["кот", "дом", "кот", "сад", "дом"]
unique_first_letters = {w[0] for w in words}
print(unique_first_letters)
# {'к', 'д', 'с'} -- порядок не гарантирован
w[0] — первый символ каждого слова (индекс уже знакомый вам приём). Слова "кот" встречаются дважды, поэтому буква "к" тоже вычислится дважды — но множество по своей природе не хранит повторы, так что в unique_first_letters она останется в единственном экземпляре.
Как отличить пустые скобки: квадратные [] всегда дают список. Фигурные с двоеточием — словарь: {k: v for ...}. Фигурные без двоеточия — множество: {x for ...}. Но пустые фигурные скобки {} без всякого генератора — это всегда словарь, а не множество; для пустого множества нужна отдельная запись set(), которую вы уже видели в уроке про множества.
Круглые скобки: считаем без готового списка
Простыми словами: иногда список целиком вам не нужен — вы просто хотите посчитать сумму или найти максимум среди преобразованных значений и на этом закончить. Создавать полноценный список ради одного числа — лишняя трата памяти, особенно если элементов много. Для такого случая есть запись, которая не строит список заранее, а выдаёт значения по одному, по мере надобности.
Официально это называется генераторное выражение, по-английски generator expression. Синтаксис почти как у list comprehension, только квадратные скобки заменены круглыми: (выражение for элемент in коллекция).
numbers = [1, 2, 3, 4, 5]
total = sum(n ** 2 for n in numbers)
print(total)
# 55
Круглые скобки здесь можно даже не дублировать — когда генераторное выражение единственный аргумент функции, как в sum(...), его собственные скобки заменяют скобки вызова. sum() забирает значения одно за другим: сначала 1 ** 2 = 1, потом 2 ** 2 = 4, и так далее, сразу складывая их, вместо того чтобы сначала собрать список [1, 4, 9, 16, 25] целиком и лишь потом его сложить. Итог тот же — 55 — но промежуточный список в памяти ни разу не появился.
| Нужен список для дальнейшей работы (индексы, срезы, несколько проходов) | [...] list comprehension |
|---|---|
| Нужно один раз посчитать сумму, максимум, минимум или просто перебрать | (...) generator expression |
Когда лучше обычный цикл
Генератор списка хорош, когда его можно прочитать за один взгляд. Если для этого нужно вчитываться и держать в голове несколько условий сразу — лучше вернуться к обычному циклу for, который вы уже хорошо знаете.
# Трудно читать с первого раза
result = [x * 2 for x in range(100) if x % 2 == 0 if x % 3 == 0 if x > 10]
# Понятно даже через неделю
result = []
for x in range(100):
if x % 2 == 0 and x % 3 == 0 and x > 10:
result.append(x * 2)
Оба варианта делают одно и то же, но во втором сразу видно, какие условия проверяются и что происходит при их выполнении. В генераторе списка три подряд идущих if читаются гораздо тяжелее.
Частые ошибки
Списки вместо действий (побочные эффекты). Генератор списка создан для того, чтобы построить коллекцию, а не для того, чтобы что-то напечатать или изменить снаружи. Если внутри выражения стоит print(), получаете нежелательный побочный список:
[print(x) for x in range(3)]
# Печатает 0, 1, 2 -- но заодно создаёт список [None, None, None],
# потому что print() всегда возвращает None, а он ведь тоже "кладётся" в список
Числа действительно напечатаются, но заодно втихую создастся и тут же останется невостребованным список из трёх None — ведь print() ничего полезного не возвращает, а генератор списка всё равно собирает то, что возвращает выражение на каждом шаге. Для печати нужен обычный цикл for x in range(3): print(x), без всякого списка.
{} — это словарь, а не множество:empty = {}
print(type(empty))
# <class 'dict'>
Даже без генератора это правило действует всегда: пустые фигурные скобки Python читает как пустой словарь, потому что словарь исторически появился раньше и занял эту короткую запись. Если нужно пустое множество, пишите set().
Что важно запомнить
[выражение for элемент in коллекция] строит список за один проход — это то же самое, что append() в цикле, только короче
if в конце — фильтр, уменьшает число элементов; тернарный X if ... else Y в начале выражения — замена значения, число элементов не меняется
Скобки определяют тип результата: [...] — список, {k: v ...} — словарь, {x ...} — множество, (...) — генераторное выражение
Два for подряд разворачивают вложенные списки в один плоский — читайте их по порядку, как вложенный цикл
Генераторное выражение не хранит все значения в памяти сразу — используйте его внутри sum(), max(), min(), когда список целиком не нужен
Если условий стало больше двух-трёх и запись трудно прочитать — возвращайтесь к обычному циклу for, читаемость важнее краткости
Проверьте себя
5 вопросов
Квадраты чётных чисел
Дан список чисел. С помощью генератора списка создайте новый список, содержащий квадраты только чётных чисел из исходного списка.
numbers = [3, 8, 1, 12, 5, 6, 10, 7]Выведите результат. Ожидаемый вывод: [64, 144, 36, 100]