Модуль 4 · Словари, множества и кортежи

Справочник по Python · модуль 4 · словари, множества и кортежи

Глава 4.4

4.4

Методы словарей:
часть 2

Эта глава завершает рабочий набор dict: динамические представления, которые ведут себя как множества, безопасное удаление, создание значения при отсутствии ключа и пакетное обновление.

Категория
Словари
Уровень
Средний
Связанные темы
Представления, множества, группировка
Зачем это знать
Изменять словарь безопасно и сравнивать словари между собой

§1Представление, а не список

Три метода — keys(), values() и items() — возвращают представление: живое окно в словарь. Это не копия и не список, и путаница здесь стоит времени.

СвойствоСписокПредставление
доступ по индексуестьнет
перебор цикломестьесть
len() и inестьесть
отражает изменения словарянетда
занимает память под копиюданет

§2keys(): ключи

main.pyчто именно возвращается
user = {"name": "Max", "role": "admin"}
keys = user.keys()

print(keys)
print("role" in keys)
print(len(keys))
Выводобъект dict_keys
dict_keys(['name', 'role'])
True
2

Для обычного перебора keys() не нужен: for key in user даёт то же самое и короче. Метод полезен, когда область нужно назвать явно или когда представление сохраняют в переменную.

§3Представление живое

Главное отличие от списка: представление не запоминает содержимое, а смотрит в словарь в момент обращения.

main.pykeys() вызван до добавления пары
user = {"name": "Max"}
keys = user.keys()

user["role"] = "admin"

print(keys)
Выводновый ключ появился сам
dict_keys(['name', 'role'])

Это удобно, когда представление нужно один раз завести и дальше сверяться с ним. И это же объясняет RuntimeError из главы 4.2: цикл идёт по живому представлению, поэтому добавление пары ломает перебор. Чтобы зафиксировать состояние, делают снимок: list(user.keys()).

§4values() и items()

Два других представления устроены так же и отличаются только содержимым.

main.pyагрегация значений
stock = {"books": 12, "pens": 30, "bags": 4}

print(sum(stock.values()))
print(min(stock.values()))
Выводвсего штук и наименьший остаток
46
4
main.pyотбор по значению
for product, amount in stock.items():
    if amount < 10:
        print(product, amount)
Выводзаканчивается только одна позиция
bags 4

§5Представления как множества

Неочевидная возможность: keys() и items() поддерживают операции множеств из главы 4.7. Ключи ведь уникальны — значит, ведут себя как множество.

main.pyсравнение двух словарей по ключам
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Практика: что изменилось

Из предыдущего параграфа получается готовый способ сравнить две версии настроек — без единого цикла.

main.pyкакие пары изменились или исчезли
old = {"name": "Max", "role": "user"}
new = {"name": "Max", "role": "admin"}

print(old.items() - new.items())
Выводроль была другой
{('role', 'user')}

Пара ("name", "Max") есть в обоих словарях и потому в разность не попала. Работает это только тогда, когда значения хешируемы: пара со списком внутри вызовет знакомую ошибку из главы 4.1.

§7pop(): удалить и забрать

Метод удаляет пару и возвращает её значение — в отличие от del, который просто удаляет.

main.pyвынуть пароль из данных запроса
payload = {"name": "Max", "password": "secret"}
password = payload.pop("password", None)

print(password)
print(payload)
Выводзначение получено, пара удалена
secret
{'name': 'Max'}

Второй аргумент — запасное значение. С ним отсутствие ключа перестаёт быть ошибкой.

main.pyключа нет
data = {"name": "Max"}
print(data.pop("nope", "запасное"))
Выводвместо KeyError
запасное

§8del или pop

ИнструментКогда удобенЕсли ключа нет
del data[key]нужно только удалитьKeyError
data.pop(key)нужно забрать значениеKeyError
data.pop(key, default)ключ может отсутствоватьвернётся default

Это та же тройка, что у списков в главе 3.11: del — оператор, pop() — метод, возвращающий удалённое. Разница лишь в том, что здесь выбор идёт по ключу.

§9popitem()

Метод снимает последнюю добавленную пару и возвращает её кортежем.

main.pyрезультат сразу распаковывается
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(): прочитать или создать

Метод возвращает значение ключа, а если ключа нет — сначала создаёт его с указанным значением.

main.pyдва состояния ключа
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Группировка

Классическая задача: разложить плоский список записей по категориям.

main.pyгорода по странам
rows = [("Россия", "Москва"), ("Казахстан", "Астана"), ("Россия", "Казань")]
grouped = {}

for country, city in rows:
    grouped.setdefault(country, []).append(city)

print(grouped)
Выводсписки собраны по ключам
{'Россия': ['Москва', 'Казань'], 'Казахстан': ['Астана']}
  1. setdefault(country, []) для новой страны создаёт пустой список и возвращает его.
  2. Для уже известной страны — возвращает существующий список, ничего не создавая.
  3. append(city) в обоих случаях работает с полученным списком.

Здесь важно, что метод возвращает сам список, а не копию: append() меняет то, что лежит в словаре. С get(country, []) приём бы не сработал — тот вернул бы новый пустой список, не связанный со словарём, и добавление потерялось бы.

§13update(): пакетное обновление

Метод переносит пары из другого источника в текущий словарь: существующие ключи обновляет, новые добавляет.

main.pyнастройки пользователя поверх базовых
config = {"theme": "light", "language": "en"}
config.update({"theme": "dark", "font_size": 16})

print(config)
Выводтема заменена, размер добавлен
{'theme': 'dark', 'language': 'en', 'font_size': 16}

Источником может быть не только словарь.

main.pyтри формы вызова
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 два словаря можно объединить оператором. Он не меняет исходные, а строит новый.

main.pyобъединение без изменения исходных
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 попадёт Nonedata.update(other) отдельной строкой
data.pop("x") без гарантии ключаKeyErrordata.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, а оператор | строит новый, отдавая приоритет правому словарю.

Глава 4.4 · Методы словарей II · Справочник по Python Автор: Макаров Максим Николаевич