Словари: dict, методы, get, comprehension, упорядоченность
dict - вторая по важности коллекция после list. Это hash-таблица для пар ключ-значение, с амортизированным O(1) для доступа, вставки и удаления. В Python 3.7+ dict сохраняет порядок вставки. В этом уроке - все основные операции, идиомы и подводные камни.
Создание
# Литерал
user = {"name": "Alice", "age": 30, "email": "a@b.c"}
# Пустой
empty = {} # dict (не set!)
empty2 = dict()
# Из пар
data = dict([("a", 1), ("b", 2)]) # {'a': 1, 'b': 2}
# Из keyword-аргументов
config = dict(host="localhost", port=5432)
# Из zip
keys = ["name", "age"]
values = ["Alice", 30]
user = dict(zip(keys, values))
# Из comprehension
squares = {x: x ** 2 for x in range(5)} # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# Через fromkeys - один и тот же дефолт для всех
flags = dict.fromkeys(["a", "b", "c"], False) # {'a': False, 'b': False, 'c': False}
d = dict.fromkeys(["a", "b"], [])
d["a"].append(1)
print(d) # {'a': [1], 'b': [1]} - shared!
Правильно через comprehension:
d = {k: [] for k in ["a", "b"]}
Доступ и модификация
user = {"name": "Alice", "age": 30}
# Чтение
user["name"] # 'Alice'
user["missing"] # KeyError
# Безопасное чтение
user.get("name") # 'Alice'
user.get("missing") # None
user.get("missing", "default") # 'default'
# Запись
user["email"] = "a@b.c"
user.update({"age": 31, "city": "Moscow"}) # bulk update
# Удаление
del user["age"]
removed = user.pop("name") # удалить и вернуть
removed = user.pop("missing", "default")
last_pair = user.popitem() # последнюю пару (LIFO с Py 3.7)
user.clear() # очистить
get - идиоматичный безопасный доступ. Используй вместо try/except KeyError или if key in dict.
Проверка наличия
"name" in user # True - проверка ключа
"Alice" in user # False - проверяется ключ, не значение
"Alice" in user.values() # True - значение
# Проверка пар
("name", "Alice") in user.items() # True
in для dict работает за O(1). in user.values() - линейный O(n).
setdefault и defaultdict
Часто нужно: «если ключа нет, создай со значением по умолчанию, потом верни/измени».
# Через setdefault - один встроенный метод
data = {}
data.setdefault("users", []).append("Alice")
data.setdefault("users", []).append("Bob")
print(data) # {'users': ['Alice', 'Bob']}
setdefault возвращает существующее значение или вставляет дефолтное.
Альтернатива - collections.defaultdict:
from collections import defaultdict
data = defaultdict(list)
data["users"].append("Alice") # ключа нет - создаст []
data["users"].append("Bob")
data["roles"].append("admin") # автоматически создан
print(dict(data)) # {'users': ['Alice', 'Bob'], 'roles': ['admin']}
Используй defaultdict когда нужно много ленивых вставок, setdefault для разовых случаев. Подробнее про этот и другие контейнеры - в уроке про collections.
Итерация
data = {"name": "Alice", "age": 30, "email": "a@b.c"}
# По ключам (по умолчанию)
for key in data:
print(key)
# По значениям
for value in data.values():
print(value)
# По парам
for key, value in data.items():
print(f"{key} = {value}")
items() - идиоматичный способ когда нужны оба. Не используй for key in d: d[key] - это лишний lookup.
Объединение словарей
a = {"x": 1, "y": 2}
b = {"y": 20, "z": 30}
# update - изменяет первый
a.update(b)
print(a) # {'x': 1, 'y': 20, 'z': 30}
# | operator (Python 3.9+) - новый dict
merged = a | b
print(merged) # {'x': 1, 'y': 20, 'z': 30}
# unpacking - новый dict, любые версии Python
merged = {**a, **b}
При коллизии ключей позднейший выигрывает. b затирает значения из a.
Упорядоченность
С Python 3.7 dict гарантированно сохраняет порядок вставки. Это часть языковой спецификации, не деталь реализации.
d = {"first": 1, "second": 2, "third": 3}
list(d.keys()) # ['first', 'second', 'third'] - порядок гарантирован
Раньше для упорядоченности использовали collections.OrderedDict. Теперь обычно достаточно dict. OrderedDict остаётся полезен только для:
- Сравнения с учётом порядка (
OrderedDict ==сравнивает и порядок) - Метода
move_to_end()
Comprehensions для dict
# Обратная карта
data = {"a": 1, "b": 2, "c": 3}
reversed_map = {v: k for k, v in data.items()}
# {1: 'a', 2: 'b', 3: 'c'}
# Фильтрация
positive = {k: v for k, v in data.items() if v > 1}
# Трансформация значений
doubled = {k: v * 2 for k, v in data.items()}
# Группировка через словарь
words = ["apple", "ant", "banana", "berry"]
by_letter = {}
for word in words:
by_letter.setdefault(word[0], []).append(word)
# {'a': ['apple', 'ant'], 'b': ['banana', 'berry']}
dict как замена elif
Если нужно выбрать значение по ключу:
# elif
def get_label(grade):
if grade == "A":
return "Отлично"
elif grade == "B":
return "Хорошо"
elif grade == "C":
return "Удовлетворительно"
else:
return "Неизвестно"
# dict
def get_label(grade):
labels = {"A": "Отлично", "B": "Хорошо", "C": "Удовлетворительно"}
return labels.get(grade, "Неизвестно")
Декларативнее и легче расширять, чем цепочка elif. Особенно при множестве веток или динамически меняющемся списке.
То же для dispatch на функции:
def handle_create(data): ...
def handle_update(data): ...
def handle_delete(data): ...
handlers = {
"create": handle_create,
"update": handle_update,
"delete": handle_delete,
}
def dispatch(action, data):
handler = handlers.get(action)
if handler is None:
raise ValueError(f"Unknown action: {action}")
return handler(data)
Что может быть ключом
Только hashable объекты:
- Числа (
int,float,complex) - Строки (
str,bytes) - Tuple из hashable
- frozenset
- Большинство пользовательских классов (по умолчанию)
Mutable не подходят:
data = {}
data[[1, 2]] = "value" # TypeError - list unhashable
data[{1, 2}] = "value" # TypeError - set unhashable
data[(1, 2)] = "value" # OK - tuple hashable
Это и в уроке про изменчивость разбирали. Для составных ключей - tuple или frozenset.
Performance
| Операция | Сложность |
|---|---|
d[k] доступ | O(1) средняя |
d[k] = v вставка | O(1) средняя |
del d[k] | O(1) средняя |
k in d | O(1) средняя |
len(d) | O(1) |
d.values() поиск | O(n) |
| Итерация | O(n) |
«Средняя» означает амортизированную - редко O(n) при коллизиях или resize hash-таблицы. Как устроена сама хеш-таблица, разбирается в уроке «Map изнутри». Для backend-задач можно считать что dict-операции константные.
Идиомы
Подсчёт частот:
from collections import Counter
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counts = Counter(words)
print(counts) # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
print(counts.most_common(2)) # [('apple', 3), ('banana', 2)]
Counter подробно в уроке 26.
Merge без перезатирания:
defaults = {"timeout": 30, "retries": 3}
user_config = {"timeout": 60}
# user_config приоритет
final = {**defaults, **user_config}
# {'timeout': 60, 'retries': 3}
Конвертация в dict:
# Из строки key=value
"a=1,b=2".split(",") # ['a=1', 'b=2']
dict(item.split("=") for item in "a=1,b=2".split(",")) # {'a': '1', 'b': '2'}
Подводные камни
1. {} это dict, не set
empty_dict = {}
empty_set = set() # для пустого set нужен явный конструктор
2. Изменение dict во время итерации
for k in data:
if data[k] < 0:
del data[k] # RuntimeError: dictionary changed size during iteration
Используй копию ключей:
for k in list(data.keys()):
if data[k] < 0:
del data[k]
Или dict comprehension:
data = {k: v for k, v in data.items() if v >= 0}
3. Mutable default
def add_to_group(item, groups={}):
groups.setdefault("default", []).append(item)
return groups
# Те же проблемы что с list - shared между вызовами
Используй None и инициализируй внутри.
4. get vs []
data = {"key": None}
data["key"] # None - есть ключ со значением None
data["missing"] # KeyError - нет ключа
data.get("key") # None - есть ключ
data.get("missing") # None - нет ключа
# Различить через `in`
"missing" in data # False
get не различает «ключа нет» и «значение None». Если важно - используй in или try/except KeyError.
Сравнение с Go и PHP
В Go map[string]int{"a": 1, "b": 2} - похоже на Python dict, но требует объявления типов ключа и значения. Также не сохраняет порядок:
m := map[string]int{"a": 1, "b": 2}
v, ok := m["a"] // ok - false если нет ключа
В PHP ассоциативные массивы это и dict, и list одновременно. Сохраняют порядок:
$data = ['name' => 'Alice', 'age' => 30];
$data['email'] = 'a@b.c';
Python dict ближе к PHP по гибкости (сохраняет порядок, не требует типизации) и к Go по семантике (отдельный тип от list).
Мини-задание
- Безопасный доступ через get:
config = {"host": "localhost"}
host = config.get("host", "0.0.0.0")
port = config.get("port", 5432)
debug = config.get("debug", False)
print(host, port, debug) # localhost 5432 False
- Группировка через defaultdict:
from collections import defaultdict
orders = [
{"user": "Alice", "total": 100},
{"user": "Bob", "total": 50},
{"user": "Alice", "total": 200},
{"user": "Bob", "total": 75},
]
by_user = defaultdict(list)
for order in orders:
by_user[order["user"]].append(order["total"])
print(dict(by_user))
# {'Alice': [100, 200], 'Bob': [50, 75]}
- Dict comprehension для конвертации:
# Перевернуть отображение
ru_en = {"один": "one", "два": "two", "три": "three"}
en_ru = {v: k for k, v in ru_en.items()}
print(en_ru)
# {'one': 'один', 'two': 'два', 'three': 'три'}
Что дальше
Освоили dict. В следующем уроке - множества (set): коллекции уникальных элементов с быстрым поиском и операциями объединения, пересечения, разности.