$ sudo teach IT
Модуль 5 · Коллекции · Урок 5.8

Генераторы списков и словарей

Строим списки и словари одной строкой вместо пяти — и учимся не увлекаться, когда это перестаёт быть читаемым.

Теория + практика~35 минутНовичок

К этому моменту вы уже умеете создавать список пустым и заполнять его в цикле: завести 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]

Длина слов через dict comprehension

Premium

Плоский список из матрицы

Premium