dict comprehension: словарь за строку
Dict comprehension собирает словарь за одну строку: квадраты чисел, цены со скидкой, фильтр по условию, инверсия ключей и значений — и словарь из списка через enumerate.
Редакция Питоники
Словари в этом разделе собирались по кусочку: пара за парой в литерале, добавлением через присваивание, пачкой через update и |. Но чаще всего словарь строится из данных, которые уже есть: из списка чисел, из старого словаря, из пар, перебранных в цикле. Для этого случая у Python есть короткая запись — dict comprehension: цикл, свёрнутый в одну строку, который собирает словарь целиком.
От цикла к comprehension: квадраты чисел
squares = {n: n * n for n in range(1, 7)}
print(squares)
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36}Читается запись по частям. Внутри фигурных скобок стоит пара n: n * n — что положить в словарь: слева от двоеточия ключ, справа значение. Дальше for n in range(1, 7) — откуда брать элементы: comprehension пройдёт по каждому n и для каждого вычислит пару. Тот же словарь циклом выглядел бы так: пустой словарь, for, присваивание squares[n] = n * n — пять строк против одной, и разница растёт с каждой задачей.
Цены со скидкой: преобразование и фильтр
prices = {'молоко': 89, 'хлеб': 42, 'сыр': 590}
sale = {name: round(price * 0.9) for name, price in prices.items()}
print(sale)
{'молоко': 80, 'хлеб': 38, 'сыр': 531}prices = {'молоко': 89, 'хлеб': 42, 'сыр': 590, 'чай': 310}
cheap = {name: price for name, price in prices.items() if price < 100}
print(cheap)
{'молоко': 89, 'хлеб': 42}В обоих случаях после for стоят два имени: перебор идёт по items, поэтому каждая итерация распаковывается в name и price. До for записывают, что положить в пару, после for — из чего берутся элементы, а условие после if отсеивает лишнее. Скидка меняет значение — выражение слева может быть любым вычислением; фильтр отбирает пары целиком — условие возвращает False, и пара в новый словарь не попадает. Исходный prices при этом не тронут: comprehension всегда строит новый словарь.
Инверсия: ключ и значение меняются местами
phone = {'Анна': '101', 'Борис': '102', 'Вера': '103'}
by_number = {number: name for name, number in phone.items()}
print(by_number)
print(by_number['103'])
{'101': 'Анна', '102': 'Борис', '103': 'Вера'}
ВераИнверсия, которая не теряет данные
Раз замена значений ключами может съесть данные, нужен обратный ход: собрать словарь, где по значению лежит не одно имя, а список всех имён с таким значением. Здесь ключ остаётся уникальным — это само значение, — а потерять несколько имён уже невозможно. Синтаксис получается плотнее, но смысл тот же: один проход вместо цикла с проверками.
salaries = {'Анна': 90, 'Борис': 90, 'Вера': 120}
by_salary = {}
for name, value in salaries.items():
by_salary.setdefault(value, []).append(name)
print(by_salary)
{90: ['Анна', 'Борис'], 120: ['Вера']}Разбор по шагам. by_salary.setdefault(value, []) достаёт список по ключу value, а если его нет — создаёт пустой и кладёт на место. Дальше .append(name) добавляет имя в этот список. Получается группировка: по ключу-зарплате собирается список имён. Тот же результат даёт defaultdict(list) из урока 12 — короче, потому что не нужно проверять наличие ключа.
from collections import defaultdict
salaries = {'Анна': 90, 'Борис': 90, 'Вера': 120}
grouped = defaultdict(list)
for name, value in salaries.items():
grouped[value].append(name)
print(dict(grouped))
{90: ['Анна', 'Борис'], 120: ['Вера']}Одно отличие, которое стоит знать: defaultdict создаёт пустой список при обращении к отсутствующему ключу, даже если вы просто читаете. Поэтому в конце мы печатаем dict(grouped) — обычный словарь, без побочного эффекта. Если бы ключ '130' не встречался в данных, но мы его запросили, в grouped появился бы лишний пустой список, и длина словаря разъехалась бы с числом реальных групп.
Существует и однострочная версия той же группировки — вложенный comprehension. Работает она медленнее цикла (внутренний список перебирает весь словарь для каждого значения), но записывается так:
salaries = {'Анна': 90, 'Борис': 90, 'Вера': 120}
grouped = {value: [name for name, v in salaries.items() if v == value]
for value in salaries.values()}
print(grouped)
{90: ['Анна', 'Борис'], 120: ['Вера']}Читается в два слоя. Внешний comprehension перебирает salaries.values() — то есть уникальные зарплаты, — и для каждой строит список имён. Внутренний comprehension отбирает все пары, у которых зарплата совпадает с текущей. Результат тот же, что у цикла, и данные не теряются. Но если в словаре 50 тысяч пар, внутренний перебор будет выполнен 50 тысяч раз — вот здесь цикл с setdefault уже выигрывает, и разница станет заметной. Правило простое: пара-три тысячи записей — comprehension, больше — цикл.
Словарь из списка: enumerate и zip
Comprehension умеет брать не только из словаря. Если под рукой список, ключ можно построить из индекса — и вот тут пригодится enumerate: он отдаёт пары «номер, элемент», нумеруя с нуля.
words = ['сок', 'компот', 'чай', 'кофе']
print({i: w for i, w in enumerate(words)})
print({w: i for i, w in enumerate(words)})
{0: 'сок', 1: 'компот', 2: 'чай', 3: 'кофе'}
{'сок': 0, 'компот': 1, 'чай': 2, 'кофе': 3}Второй вариант — инверсия списка: по слову находится его номер. Обратите внимание на порядок переменных в comprehension: for i, w in enumerate(words) даёт сначала индекс, потом элемент, и в фигурных скобках вы вправе поставить их как угодно. В первой строке индекс стал ключом, во второй — значением. Если нужен нумерация с единицы, добавьте start=1 — часто это удобнее для нумерации строк таблицы.
print({i: letter for i, letter in enumerate('абв', start=1)})
{1: 'а', 2: 'б', 3: 'в'}Второй случай — два списка одинаковой длины, которые надо разложить по парам. Здесь работает zip: он соединяет списки по позициям, и получается готовый словарь «имя — зарплата».
names = ['Анна', 'Борис']
salaries = [90, 120]
print({n: s for n, s in zip(names, salaries)})
{'Анна': 90, 'Борис': 120}Оговорка про zip, на которой спотыкаются: он останавливается на самом коротком списке. Если в одном из списков окажется лишний элемент, он молча потеряется — никакой ошибки, никакого предупреждения. Проверяйте длины заранее, если данные приходят из разных источников: assert len(names) == len(salaries). Для словарей вместо списков zip работает ещё удобнее: он соединяет пары items() напрямую.
Ключ из значения: когда имена у списка нет
Иногда словарь нужен не для красоты, а чтобы обратиться по другому полю. Пример из практики: в логе лежит список пар «код — имя», а искать надо по имени. Comprehension разворачивает список пар в словарь, где ключ — код, а значение — имя; перевернуть можно вторым проходом.
pairs = [(1, 'Анна'), (2, 'Борис')]
print({name: code for code, name in pairs})
{'Анна': 1, 'Борис': 2}Здесь for code, name in pairs распаковывает кортежи прямо в цикле, и слева от двоеточия стоит name — то есть второй элемент кортежа. В исходном списке первым шёл код, а стал он значением: имя теперь ключ, по которому ищем код. Обратный вариант получится простой перестановкой в фигурных скобках: {code: name for code, name in pairs} даст словарь «код — имя». Проверить, что разворот прошёл правильно, можно сравнением длин: у списка пар длина равна числу кортежей, а у словаря — числу уникальных ключей, и если в списке были повторы имён, словарь получится короче.
Тернарный оператор: значение зависит от условия
До сих пор условие в comprehension только отсеивало пары: не прошёл — не попал в словарь. А если значение должно зависеть от условия, нужен тернарный оператор — компактный если то, иначе то. Пишется он прямо в выражении значения.
prices = {'молоко': 89, 'хлеб': 42, 'сыр': 590}
sale = {name: round(price * 0.9) if price > 50 else price
for name, price in prices.items()}
print(sale)
{'молоко': 80, 'хлеб': 42, 'сыр': 531}Читается так: если цена больше 50, в пару кладётся округлённая цена со скидкой, иначе — сама цена без изменений. Хлеб за 42 остался нетронутым, молоко со 89 превратилось в 80, сыр — в 531. Тернарный оператор — это выражение, его значение можно использовать дальше как обычное число. Второй вариант записи, круглыми скобками: {name: (round(price * 0.9) if price > 50 else price) for ...} — так его ещё проще отличить от условия-фильтра после for.
Когда comprehension — уже слишком
Comprehension — не лучший стиль на все случаи. Как только внутри появляются два уровня вложенности, несколько условий или вычисление с приоритетом, строка перестаёт читаться с одного взгляда. Правило простое: если запись помещается в одну строку шириной экрана — оставляйте, если нет — возвращайтесь к обычному циклу. Читаемость важнее компактности.
matrix = [[1, 2], [3, 4]]
cells = {(r, c): matrix[r][c]
for r in range(len(matrix))
for c in range(len(matrix[r]))}
print(cells)
{(0, 0): 1, (0, 1): 2, (1, 0): 3, (1, 1): 4}Здесь получился словарь-таблица: ключ-кортеж (строка, столбец), значение — ячейка. Такой приём удобен для разреженных матриц и для игровых полей. Запись разбита на три строки, и каждый кусок на своей — так её ещё можно читать. Но если бы в comprehension понадобился ещё один уровень, мы бы честно переписали это обычным циклом: код, который нужно читать дважды, должен читаться легко с первого раза.
| Задача | Запись | Результат |
|---|---|---|
| Словарь из списка | {i: w for i, w in enumerate(words)} | Индекс как ключ |
| Инверсия списка | {w: i for i, w in enumerate(words)} | Слово как ключ, номер как значение |
| Словарь из двух списков | {n: s for n, s in zip(names, salaries)} | Пары по позициям |
| Новые ключи с нулевым значением | dict.fromkeys(keys, 0) | Словарь-заготовка |
| Пересчёт с условием | {k: v * 0.9 if v > 50 else v for ...} | Значение зависит от пары |
| Фильтр с двумя условиями | {k: v for ... if v > 0 and k != 'x'} | Отбор по двум признакам |
| Инверсия с сохранением всех имён | {v: [k for k, vv in d.items() if vv == v] for v in d.values()} | Значение — список ключей |
Что дальше
Comprehension собирает словарь из данных, которые уже в программе. Дальше — словарь, который приходит снаружи: в уроке 16 разбираем JSON и json.loads, который превращает текст в словарь, а json.dumps — обратно. А если данные надо сначала разобрать на части, посмотрите урок про вложенные словари — там цепочка ключей и безопасное чтение через get.
Сначала предскажи ответ в голове — это главный навык программиста.
d = {n: n * 10 for n in (1, 3)}
print(d)
prices = {'чай': 50, 'кофе': 120}
cheap = {k: v for k, v in prices.items() if v < 100}
print(cheap)
d = {'a': 1, 'b': 2}
inv = {v: k for k, v in d.items()}
print(inv)
1. Что такое dict comprehension?
2. Что стоит слева от двоеточия в записи {n: n * n for n in range(3)}?
3. Что делает условие после if в comprehension: {name: price for ... if price < 100}?
4. Меняет ли comprehension исходный словарь prices?
5. Почему при инверсии словаря с повторяющимися значениями часть данных пропадает?
6. Как инвертировать словарь, не потеряв имена с одинаковыми значениями?
7. Что даст {name: round(price * 0.9) if price > 50 else price for name, price in prices.items()}?
8. Как из списка words получить словарь «слово — его номер»?
Дан список words = ['сок', 'компот', 'чай', 'кофе']. Собери через dict comprehension словарь «слово — длина слова» и выведи его. Затем тем же приёмом, но с фильтром, собери словарь только из коротких слов — длина не больше 3 — и выведи его.
Что такое dict comprehension в Python?
Короткая запись, которая строит словарь за один проход цикла: {n: n * n for n in range(1, 7)}. Слева от двоеточия — выражение ключа, справа — значение, после for — перебор, а условие после if отфильтровывает лишние пары. Результат — обычный словарь dict.
Урок 8: перебор словаряКак создать словарь из списка в Python?
Через dict comprehension: lengths = {w: len(w) for w in words} соберёт словарь «слово — длина» из списка words. Если нужны пары «индекс — элемент», перебирайте enumerate: {i: w for i, w in enumerate(words)}. Оба варианта дают обычный dict.
Как поменять ключи и значения местами?
Инверсией: {v: k for k, v in d.items()}. Значения становятся ключами, поэтому приём безопасен только при уникальных значениях: повторяющиеся значения претендуют на один ключ, и выживает последняя пара.
Чем dict comprehension лучше обычного цикла?
Короткостью и отсутствием лишней переменной-накопителя: вместо пустого словаря, цикла и присваиваний — одна строка. Но выигрыш только пока запись читается легко: вложенные условия и несколько for в comprehension читаются хуже, чем те же пять строк обычного цикла.
Как создать словарь из списка через enumerate?
Через enumerate: {w: i for i, w in enumerate(words)} даст «слово — номер», а {i: w for i, w in enumerate(words)} — «номер — слово». enumerate отдаёт пары «индекс, элемент», поэтому их надо распаковать в правильном порядке. Для нумерации с единицы добавьте start=1.
Как сделать словарь из двух списков?
Через zip: {n: s for n, s in zip(names, salaries)}. Важно помнить, что zip останавливается на самом коротком списке — лишние элементы теряются молча, без исключения. Если списки из разных источников, проверьте длины заранее.
Чем условие после for отличается от тернарного оператора?
Условие после for отсеивает пары: не прошла проверку — пара в словарь не попала. Тернарный оператор выбирает значение, но оставляет саму пару в словаре. В записи {k: v * 0.9 if v > 50 else v ...} все позиции останутся на месте, но у части изменится значение.
Когда обычный цикл лучше dict comprehension?
Когда запись перестаёт читаться с одного взгляда: два уровня вложенности, несколько условий, длинные выражения. И когда данных много — вложенный comprehension перебирает источник целиком для каждого значения, и на десятках тысяч пар обычный цикл с setdefault выигрывает заметно.
Урок 12: defaultdictПонравился урок? Сошлитесь на него
«До for записывают, что положить в пару, после for — из чего берутся элементы, а условие после if отсеивает лишнее.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Словари · Урок 14
Объединение словарей: update и оператор |
Два словаря в один: update переписывает на месте, оператор | собирает новый словарь, слои настроек ставятся по приоритету, а при конфликте ключей побеждает правый.
Словари · Урок 16
Словарь и JSON: json.loads и json.dumps
Словарь и JSON — два облика одних данных: json.loads читает строку в dict, json.dumps собирает строку из словаря, json.dump пишет в файл, а ensure_ascii=False бережёт кириллицу.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Циклы · Урок 12
Генератор списка в Python: цикл в одну строку
Цикл сбора списка — из трёх строк в одну: выражение, for, условие. Разбираем анатомию генератора списка, фильтр через if, превращение через if-else и границу, за которой однострочник вредит.
генератор списка pythonlist comprehension python
Datetime · Урок 7
Сравнение и сортировка дат
Даты сравниваются как числа — и потому сортируются, выбираются min и max и становятся ключами словаря. Разбираем операторы и честную сортировку без строковых сюрпризов.
даты как ключи словарясортировка списка дат
Словари · Урок 2
Создание словаря: фигурные скобки и dict()
Три способа создать словарь: литерал в фигурных скобках, dict() из списка пар и пустой словарь под наполнение. Плюс разбор, какие ключи годятся и почему fromkeys с пустым списком — ловушка.
dict python созданиесоздание словаря python