Включения словарей и множеств в Python
Освойте включения словарей, множеств и генераторные выражения в Python с примерами, синтаксисом и типичными ошибками.
Включения словарей, включения множеств и генераторные выражения расширяют компактный синтаксис включений списков на другие структуры данных. В этой главе подробно рассматривается каждая форма — синтаксис, фильтрация, вложенность, практические паттерны и случаи, когда их лучше избегать.
Включения словарей
Включение словаря создаёт новый dict из любого итерируемого объекта в одном выражении. Вместо написания цикла for, который вызывает d[key] = value на каждой итерации, вы описываете отображение лаконично внутри фигурных скобок.
Синтаксис
new_dict = {key_expr: value_expr for item in iterable}Добавьте необязательный фильтр if после итерируемого объекта:
new_dict = {key_expr: value_expr for item in iterable if condition}| Часть | Роль |
|---|---|
key_expr | Выражение, вычисляющее каждый ключ |
value_expr | Выражение, вычисляющее каждое значение |
item | Переменная цикла — принимает каждое значение из iterable по очереди |
if condition | Необязательный фильтр — пропускает элементы, где condition равно False |
Базовый пример: построение таблицы квадратов
squares = {x: x ** 2 for x in range(1, 6)}
print(squares)
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}Эквивалентный цикл for:
squares = {}
for x in range(1, 6):
squares[x] = x ** 2Оба дают одинаковый результат; форма с включением более лаконична и выражает намерение с первого взгляда.
Фильтрация записей
Оставьте только пары, удовлетворяющие условию:
prices = {"apple": 1.20, "banana": 0.50, "orange": 0.80, "grape": 2.50}
expensive = {item: price for item, price in prices.items() if price >= 1.00}
print(expensive)
# {'apple': 1.2, 'grape': 2.5}Преобразование значений
Применяйте функцию или арифметическое выражение к каждому значению:
prices = {"apple": 1.20, "banana": 0.50, "orange": 0.80}
# Apply a 10% discount and round to 2 decimal places
discounted = {item: round(price * 0.9, 2) for item, price in prices.items()}
print(discounted)
# {'apple': 1.08, 'banana': 0.45, 'orange': 0.72}Обмен ключей и значений
Инвертируйте словарь (работает корректно, когда все значения уникальны):
capitals = {"France": "Paris", "Germany": "Berlin", "Japan": "Tokyo"}
inverted = {city: country for country, city in capitals.items()}
print(inverted)
# {'Paris': 'France', 'Berlin': 'Germany', 'Tokyo': 'Japan'}Если значения не уникальны, более поздние записи перезаписывают более ранние. Используйте это только тогда, когда вы уверены, что значения образуют взаимно однозначное отображение.
Построение из двух итерируемых объектов с zip()
zip() объединяет элементы двух последовательностей в пары, что упрощает построение словаря из отдельных списков ключей и значений:
keys = ["name", "age", "city"]
values = ["Alice", 30, "Berlin"]
profile = {k: v for k, v in zip(keys, values)}
print(profile)
# {'name': 'Alice', 'age': 30, 'city': 'Berlin'}Это эквивалентно dict(zip(keys, values)), но форма с включением позволяет легко добавить фильтр или преобразовать значения одновременно.
Нормализация ключей
Распространённая практическая задача — очистка ключей словаря, например приведение всех ключей к нижнему регистру при объединении данных из разных источников:
raw = {"Name": "Alice", "AGE": 30, "City": "Berlin"}
normalised = {k.lower(): v for k, v in raw.items()}
print(normalised)
# {'name': 'Alice', 'age': 30, 'city': 'Berlin'}Условное выражение значения (тернарный оператор в значении)
Вы можете использовать тернарное выражение в позиции значения, чтобы выбирать между двумя значениями для каждого элемента:
scores = {"Alice": 95, "Bob": 62, "Carol": 78, "Dave": 45}
grades = {name: "pass" if score >= 70 else "fail" for name, score in scores.items()}
print(grades)
# {'Alice': 'pass', 'Bob': 'fail', 'Carol': 'pass', 'Dave': 'fail'}Вложенные включения словарей
Вы можете вкладывать включения для обработки многоуровневых данных. Например, извлечение одного поля из списка вложенных словарей:
students = [
{"name": "Alice", "score": 95, "grade": "A"},
{"name": "Bob", "score": 82, "grade": "B"},
{"name": "Carol", "score": 91, "grade": "A"},
]
# Build a {name: score} mapping from the list
name_to_score = {s["name"]: s["score"] for s in students}
print(name_to_score)
# {'Alice': 95, 'Bob': 82, 'Carol': 91}Держите вложенность неглубокой. Если логика становится трудной для понимания, выделите вспомогательную функцию или используйте обычный цикл for.
Включения множеств
Включение множества создаёт set в одном выражении. Поскольку множества содержат только уникальные элементы, дубликаты удаляются автоматически.
Синтаксис
new_set = {expression for item in iterable}
new_set = {expression for item in iterable if condition}Единственное визуальное отличие от включения словаря — отсутствие двоеточия: здесь одно выражение, а не пара key: value.
Базовый пример: уникальные квадраты
numbers = [1, 2, 2, 3, 3, 3, 4]
unique_squares = {x ** 2 for x in numbers}
print(unique_squares)
# {1, 4, 9, 16}Порядок вывода не гарантирован — множества не упорядочены, поэтому не полагайтесь на конкретный порядок отображения.
Дедупликация при преобразовании
Распространённый паттерн — нормализация строк и дедупликация за один шаг:
tags = ["Python", "python", "PYTHON", "Data", "data", "DATA"]
unique_tags = {tag.lower() for tag in tags}
print(unique_tags)
# {'python', 'data'}Фильтрация с условием
words = ["cat", "elephant", "dog", "rhinoceros", "ant"]
long_words = {w for w in words if len(w) > 4}
print(long_words)
# {'elephant', 'rhinoceros'}Поиск общих элементов
Включения множеств естественно сочетаются с операциями над множествами:
list_a = [1, 2, 3, 4, 5, 5, 6]
list_b = [4, 5, 6, 7, 8]
set_a = {x for x in list_a}
set_b = {x for x in list_b}
common = set_a & set_b
print(common)
# {4, 5, 6}Для простого преобразования без трансформации set(list_a) проще. Используйте включение множества, когда нужно фильтровать или преобразовывать данные в процессе конвертации.
Генераторные выражения
Генераторное выражение выглядит как включение списка, но использует круглые скобки вместо квадратных. Вместо того чтобы сразу строить всю коллекцию в памяти, оно производит одно значение за раз по требованию (лениво).
Синтаксис
gen = (expression for item in iterable)
gen = (expression for item in iterable if condition)Зачем использовать генераторное выражение?
| Сценарий | Включение списка | Генераторное выражение |
|---|---|---|
| Нужно итерировать результат несколько раз | Да | Нет — генераторы исчерпываются после одного прохода |
Нужен произвольный доступ по индексу (result[3]) | Да | Нет |
Результат передаётся в sum(), max(), any() и т.д. | Работает, но выделяет список | Предпочтительно — потоково передаёт значения без построения списка |
| Очень большая или бесконечная последовательность | Может исчерпать память | Эффективно — одно значение за раз |
Базовый пример
# List comprehension — builds the entire list in memory
squares_list = [x ** 2 for x in range(1, 6)]
print(squares_list) # [1, 4, 9, 16, 25]
# Generator expression — yields values one at a time
squares_gen = (x ** 2 for x in range(1, 6))
print(squares_gen) # <generator object <genexpr> at 0x...>
print(list(squares_gen)) # [1, 4, 9, 16, 25]Сам объект генератора — это не список; вы потребляете его, итерируя или передавая в функцию.
Передача напрямую во встроенные функции
Когда вы передаёте генераторное выражение как единственный аргумент функции, дополнительные круглые скобки можно опустить:
total = sum(x ** 2 for x in range(1, 1001))
print(total) # 333833500
maximum = max(len(word) for word in ["apple", "banana", "kiwi"])
print(maximum) # 6
any_negative = any(x < 0 for x in [1, -2, 3])
print(any_negative) # TrueПреимущество по памяти
Для больших данных генераторное выражение позволяет избежать загрузки всего в RAM:
# Simulate a large log file as a list of strings
log_lines = [f"ERROR line {i}" if i % 100 == 0 else f"INFO line {i}" for i in range(1_000_000)]
# Generator scans lines without building an intermediate list
error_count = sum(1 for line in log_lines if line.startswith("ERROR"))
print(error_count) # 10000Генераторы исчерпываются после одного прохода
Это наиболее распространённая ловушка:
gen = (x * 2 for x in range(5))
print(list(gen)) # [0, 2, 4, 6, 8]
print(list(gen)) # [] — the generator is now emptyЕсли вам нужно итерировать результат более одного раза, используйте включение списка или пересоздайте генератор.
Цепочка генераторных выражений
Генераторные выражения можно компоновать без создания промежуточных списков. Каждый этап получает значения из предыдущего:
numbers = range(1, 11)
# Stage 1: filter even numbers
evens = (x for x in numbers if x % 2 == 0)
# Stage 2: square them
even_squares = (x ** 2 for x in evens)
print(list(even_squares)) # [4, 16, 36, 64, 100]Промежуточный список не создаётся — значения проходят через конвейер по одному.
Выбор подходящей формы
| Что нужно… | Использовать |
|---|---|
| Список преобразованных/отфильтрованных значений | [expr for item in it] |
| Словарь из пар значений | {k: v for item in it} |
| Коллекция без дубликатов | {expr for item in it} |
| Память-эффективная однопроходная итерация | (expr for item in it) |
| Сложная многострочная логика для каждого элемента | Обычный цикл for |
Типичные ошибки
Включение словаря vs. включение множества. Оба используют фигурные скобки {}. Разница в том, пишете ли вы key: value (словарь) или одно expression (множество). Пустой {} всегда создаёт пустой словарь, а не пустое множество — для пустого множества используйте set().
d = {} # empty dict
s = set() # empty set — NOT {}Перезапись ключей. Если выражение ключа производит дубликаты, более поздние значения молча перезаписывают более ранние:
data = [("a", 1), ("b", 2), ("a", 99)]
d = {k: v for k, v in data}
print(d) # {'a': 99, 'b': 2} — first 'a' is goneОбласть видимости переменных. В Python 3 переменная цикла в любом включении является локальной для этого включения и не утекает во внешнюю область видимости:
x = "original"
result = {x: x.upper() for x in ["a", "b", "c"]}
print(x) # 'original' — comprehension's x did not overwrite thisОграничение читаемости. Если вам нужно более одного условия if или выражение длинное, обычный цикл for с понятными именами переменных, как правило, нагляднее:
# Hard to read
result = {k: v for k, v in data.items() if k.startswith("user_") if v is not None}
# Easier to read
result = {}
for k, v in data.items():
if k.startswith("user_") and v is not None:
result[k] = vСвязанные темы
- Включения списков — основа: синтаксис, фильтрация, вложенные циклы и когда использовать обычный цикл
for - Словари Python — основы словарей, создание и доступ
- Множества Python — создание множеств, операции и варианты использования
- Перебор словарей — все техники итерации по словарям
- Итераторы Python — как работает протокол итератора Python под капотом