Справочник по Python · модуль 4 · словари, множества и кортежи
Глава 4.4
Методы словарей:
часть 2
Эта глава завершает рабочий набор dict: динамические представления, которые ведут себя как множества, безопасное удаление, создание значения при отсутствии ключа и пакетное обновление.
- Категория
- Словари
- Уровень
- Средний
- Связанные темы
- Представления, множества, группировка
- Зачем это знать
- Изменять словарь безопасно и сравнивать словари между собой
§1Представление, а не список
Три метода — keys(), values() и items() — возвращают представление: живое окно в словарь. Это не копия и не список, и путаница здесь стоит времени.
| Свойство | Список | Представление |
|---|---|---|
| доступ по индексу | есть | нет |
| перебор циклом | есть | есть |
len() и in | есть | есть |
| отражает изменения словаря | нет | да |
| занимает память под копию | да | нет |
§2keys(): ключи
user = {"name": "Max", "role": "admin"}
keys = user.keys()
print(keys)
print("role" in keys)
print(len(keys))
dict_keys(['name', 'role']) True 2
Для обычного перебора keys() не нужен: for key in user даёт то же самое и короче. Метод полезен, когда область нужно назвать явно или когда представление сохраняют в переменную.
§3Представление живое
Главное отличие от списка: представление не запоминает содержимое, а смотрит в словарь в момент обращения.
user = {"name": "Max"}
keys = user.keys()
user["role"] = "admin"
print(keys)
dict_keys(['name', 'role'])
Это удобно, когда представление нужно один раз завести и дальше сверяться с ним. И это же объясняет RuntimeError из главы 4.2: цикл идёт по живому представлению, поэтому добавление пары ломает перебор. Чтобы зафиксировать состояние, делают снимок: list(user.keys()).
§4values() и items()
Два других представления устроены так же и отличаются только содержимым.
stock = {"books": 12, "pens": 30, "bags": 4}
print(sum(stock.values()))
print(min(stock.values()))
46 4
for product, amount in stock.items(): if amount < 10: print(product, amount)
bags 4
§5Представления как множества
Неочевидная возможность: keys() и items() поддерживают операции множеств из главы 4.7. Ключи ведь уникальны — значит, ведут себя как множество.
a = {"x": 1, "y": 2}
b = {"y": 2, "z": 3}
print(a.keys() & b.keys())
print(a.keys() - b.keys())
print(sorted(a.keys() | b.keys()))
{'y'}
{'x'}
['x', 'y', 'z']| Операция | Что даёт |
|---|---|
a.keys() & b.keys() | ключи, которые есть в обоих |
a.keys() - b.keys() | ключи только первого словаря |
a.keys() | b.keys() | все ключи обоих |
a.items() - b.items() | пары, которых нет во втором |
С values() так не выйдет — значения не уникальны, и множеством их считать нельзя. Попытка даёт TypeError: unsupported operand type(s) for &.
§6Практика: что изменилось
Из предыдущего параграфа получается готовый способ сравнить две версии настроек — без единого цикла.
old = {"name": "Max", "role": "user"}
new = {"name": "Max", "role": "admin"}
print(old.items() - new.items())
{('role', 'user')}Пара ("name", "Max") есть в обоих словарях и потому в разность не попала. Работает это только тогда, когда значения хешируемы: пара со списком внутри вызовет знакомую ошибку из главы 4.1.
§7pop(): удалить и забрать
Метод удаляет пару и возвращает её значение — в отличие от del, который просто удаляет.
payload = {"name": "Max", "password": "secret"}
password = payload.pop("password", None)
print(password)
print(payload)
secret
{'name': 'Max'}Второй аргумент — запасное значение. С ним отсутствие ключа перестаёт быть ошибкой.
data = {"name": "Max"}
print(data.pop("nope", "запасное"))
запасное
§8del или pop
| Инструмент | Когда удобен | Если ключа нет |
|---|---|---|
del data[key] | нужно только удалить | KeyError |
data.pop(key) | нужно забрать значение | KeyError |
data.pop(key, default) | ключ может отсутствовать | вернётся default |
Это та же тройка, что у списков в главе 3.11: del — оператор, pop() — метод, возвращающий удалённое. Разница лишь в том, что здесь выбор идёт по ключу.
§9popitem()
Метод снимает последнюю добавленную пару и возвращает её кортежем.
tasks = {"first": "done", "second": "ready"}
key, value = tasks.popitem()
print(key, value)
print(tasks)
second ready
{'first': 'done'}Порядок здесь не случайный, а именно обратный порядку добавления, — словарь ведёт себя как стек из главы 3.11. На пустом словаре метод вызывает KeyError: 'popitem(): dictionary is empty'.
До Python 3.7 порядок пар не гарантировался, и popitem() действительно снимал произвольную пару. В старом коде и старых учебниках встречается описание «удаляет случайный элемент» — сейчас это неверно.
§10setdefault(): прочитать или создать
Метод возвращает значение ключа, а если ключа нет — сначала создаёт его с указанным значением.
settings = {"theme": "dark"}
first = settings.setdefault("theme", "light")
second = settings.setdefault("language", "ru")
print(first, second)
print(settings)
dark ru
{'theme': 'dark', 'language': 'ru'}Ключевое: существующее значение метод не перезаписывает. Аргумент "light" в первой строке был проигнорирован, потому что "theme" уже существовал.
§11get() или setdefault()
Названия похожи, поведение различается ровно в одном: меняет ли метод словарь.
| Метод | Ключ есть | Ключа нет |
|---|---|---|
get(key, default) | вернёт значение | вернёт default, словарь не изменится |
setdefault(key, default) | вернёт значение | добавит пару и вернёт default |
Для счётчика из главы 4.2 нужен get(): там значение всё равно перезаписывается следующей строкой, и создавать ключ заранее незачем. А для группировки, наоборот, нужен setdefault() — и это следующий параграф.
§12Группировка
Классическая задача: разложить плоский список записей по категориям.
rows = [("Россия", "Москва"), ("Казахстан", "Астана"), ("Россия", "Казань")] grouped = {} for country, city in rows: grouped.setdefault(country, []).append(city) print(grouped)
{'Россия': ['Москва', 'Казань'], 'Казахстан': ['Астана']}setdefault(country, [])для новой страны создаёт пустой список и возвращает его.- Для уже известной страны — возвращает существующий список, ничего не создавая.
append(city)в обоих случаях работает с полученным списком.
Здесь важно, что метод возвращает сам список, а не копию: append() меняет то, что лежит в словаре. С get(country, []) приём бы не сработал — тот вернул бы новый пустой список, не связанный со словарём, и добавление потерялось бы.
§13update(): пакетное обновление
Метод переносит пары из другого источника в текущий словарь: существующие ключи обновляет, новые добавляет.
config = {"theme": "light", "language": "en"}
config.update({"theme": "dark", "font_size": 16})
print(config)
{'theme': 'dark', 'language': 'en', 'font_size': 16}Источником может быть не только словарь.
data = {"a": 1}
data.update({"b": 2})
data.update([("c", 3)])
data.update(d=4)
print(data)
{'a': 1, 'b': 2, 'c': 3, 'd': 4}Метод меняет словарь на месте и возвращает None — как append() и sort() у списков.
§14Оператор |
Начиная с Python 3.9 два словаря можно объединить оператором. Он не меняет исходные, а строит новый.
defaults = {"theme": "light", "language": "en"}
custom = {"theme": "dark"}
result = defaults | custom
print(result)
print(defaults)
{'theme': 'dark', 'language': 'en'}
{'theme': 'light', 'language': 'en'}При конфликте ключей побеждает правый словарь — это и делает запись defaults | custom естественной для настроек: справа то, что должно перекрывать.
§15update() или |
| Запись | Исходный словарь | Результат |
|---|---|---|
a.update(b) | изменяется | None |
c = a | b | не изменяется | новый словарь |
a |= b | изменяется | — |
Разница ровно та же, что между sort() и sorted() в главе 3.11 или между clear() и = {}: метод меняет объект, оператор строит новый. Если словарь известен под несколькими именами, выбор между ними существенный.
§16Частые ошибки
| Что написано | Что произойдёт | Как правильно |
|---|---|---|
data.keys()[0] | TypeError: у представления нет индексов | list(data)[0] |
result = data.update(other) | в result попадёт None | data.update(other) отдельной строкой |
data.pop("x") без гарантии ключа | KeyError | data.pop("x", None) |
get(key, []).append(x) для группировки | добавление потеряется: список не в словаре | setdefault(key, []).append(x) |
ждать, что setdefault() обновит значение | существующий ключ не меняется | обычное присваивание data[key] = value |
a.values() & b.values() | TypeError: значения не уникальны | сравнивать keys() или items() |
считать popitem() случайным выбором | снимается последняя добавленная пара | выбирать пару по ключу через pop() |
| перебирать представление и менять словарь | RuntimeError: представление живое | сделать снимок list(data) |
§17Проверьте себя
Чем представление отличается от списка?
Оно связано с исходным словарём и отражает его изменения, не занимает память под копию и не поддерживает индексы.
Почему keys() поддерживает операции множеств, а values() — нет?
Ключи уникальны и потому ведут себя как множество. Значения могут повторяться, и объединять их как множество бессмысленно.
Когда setdefault() меняет словарь?
Только когда ключа ещё нет: тогда пара добавляется. Существующее значение метод не перезаписывает.
Почему группировка работает через setdefault(), а не get()?
setdefault() возвращает список, который уже лежит в словаре, и append() меняет именно его. get() вернул бы новый пустой список, не связанный со словарём.
Что возвращает update()?
None. Метод меняет словарь на месте, поэтому присваивать его результат нельзя.
Какой словарь побеждает при конфликте в a | b?
Правый. Поэтому defaults | custom — естественный порядок для настроек.
§18Шпаргалка
data.keys()data.values()data.items()list(data)a.keys() & b.keys()old.items() - new.items()data.pop(key, None)data.setdefault(key, [])d.setdefault(k, []).append(v)data.update(other)new = a | bИтог главы
Представления — живые окна в словарь: они отражают его изменения, не поддерживают индексы и объясняют, почему нельзя менять словарь во время перебора. Ключи и пары дополнительно ведут себя как множества, что позволяет сравнивать словари без циклов. pop() с запасным значением делает удаление безопасным, setdefault() отличается от get() тем, что создаёт ключ — и потому именно он годится для группировки. update() меняет словарь и возвращает None, а оператор | строит новый, отдавая приоритет правому словарю.