Использование словарей

Синтаксис

Как и другие встроенные коллекции, словари поддерживаются языком и имеют собственный синтаксис для описания литералов.

Литерал словаря записывается в фигурных скобках. Пары «ключ-значение» разделяются запятыми, а ключ отделяется от значения двоеточием:

dictionary = {
    "one": "1",
    "two": 2,
    "items": {1: "apple", 2: "orange", 3: "grape"},
    True: "Python
    ",
}
dictionary  # {'one': '1', 'two': 2, 'items': {1: 'apple', 2: 'orange', 3: 'grape'}, True: 'Python'}

В этом примере есть и ключи-строки, и ключи-числа, и вложенные словари. И конечно же, здесь есть переменные, которые могут выступать в роли значений и ключей:

key, val = "x", 3
{key: val}  # {'x': 3}

Доступ по ключу

Выше мы объявили словарь dictionary. Запросить у него значение по ключу можно так:

dictionary["two"]  # 2
dictionary["four"]  # KeyError: 'four'

Ключа "four" в dictionary нет, поэтому выбросилось исключение KeyError — аналог IndexError для списков.

Проверить наличие ключа в словаре можно с помощью привычного оператора in:

"two" in dictionary  # True
"four" in dictionary  # False

Если вы захотите получить значение по ключу, которого может и не быть, можно сделать это так:

dictionary["four"] if "four" in dictionary else None

Подобный безопасный запрос элементов нужен довольно часто. Поэтому объект словаря имеет для этого специальный метод .get:

dictionary.get("two")  # 42
dictionary.get("four")  # Вернет None
dictionary.get("four", "no such key")  # 'no such key'

Третий вызов метода показывает, как можно явно задать значение по умолчанию. Если его не указывать, метод вернет None при отсутствии значения по указанному ключу.

Методы keys, values и items

Если попробовать проитерировать словарь, то мы получим перечень ключей:

for k in {"a": 1, "b": 2}:
    print(k)
# => a
# => b

Этого же результата можно добиться и более явно. Для этого нужно вызвать метод .keys():

print({"a": 1, "b": 2}.keys())  # dict_keys(['a', 'b'])

Чтобы получить значения, нужно вызвать метод .values():

print({"a": 1, "b": 2}.values())  # dict_values([1, 2])

Чтобы получить одновременно и ключи, и соответствующие значения, можно вызвать метод .items():

for k, v in {"a": 1, "b": 2}.items():
    print(k, "=", v)

Изменение данных в словаре

Словарь в Python — изменяемый или mutable. Но для добавления новой пары «ключ-значение» не нужны отдельные методы, вроде спискового метода .append — достаточно обычного присваивания:

d = {}  # пустой словарь
d["a"] = 1
print(d)  # => {"a": 1}
d["b"] = 2
d["a"] = 0
print(d)  # => {"a": 0, "b": 2}

Здесь вы можете увидеть, что присваивание значения новому ключу выглядит точно так же, как и присваивание существующему.

Метод pop

Удаление элементов из словаря можно сделать с помощью метода pop — в этом словарь уже больше похож на список. Только вместо индекса используется ключ:

d = {"a": 0, "b": 2}
d.pop("a")  # 0
print(d)  # {"b": 2}
d.pop("c")  # KeyError: "c"

Этот пример показывает, что будет, если попытаться извлечь значение по несуществующему ключу — мы получим исключение. Однако метод pop можно вызывать с указанием значения по умолчанию. В этом случае при отсутствии ключа в словаре будет возвращено это самое значение, а исключение "выброшено" не будет:

d = {"a": 1, "b": 2}
d.pop("3", None)  # None
d.pop("3", 33)  # 33

Аналогом спискового pop без аргументов для словаря служит метод popitem. Этот метод извлекает ключ и значение в виде кортежа, а если словарь уже пуст, то "выбрасывает" исключение:

d = {"a": 1}
d.popitem()  # ("a", 1)
d.popitem()  # KeyError: "popitem(): dictionary is empty"

В Python, начиная с версии 3.7, гарантирован порядок LIFO - Last In First Out. Это значит, что пары будут извлекаться в порядке обратном добавлению, то есть последняя добавленная пара, будет извлечена первой. При этом мы можем быть уверены в том, что:

  • Все пары будут извлечены
  • Каждая пара будет извлечена строго один раз

Дополнение одного словаря другим

У списка есть метод extend, который расширяет один список другим. У словаря есть похожий по смыслу метод update. Но при вызове update ассоциированный объект словаря не просто получает пары «ключ-значение» из нового словаря. Происходит именно обновление данных — поэтому метод и называется update. Работает это так:

  • Новые ключи дописываются в словарь
  • Если какие-то ключи уже существовали до этого, то связанные с ними значения будут заменены новыми

Так это выглядит в коде:

cart = {"apples": 5, "oranges": 7}
addon = {"oranges": 4, "lemons": 2}
cart.update(addon)
cart  # {"apples": 5, "oranges": 4, "lemons": 2}

В коде выше мы добавили лимоны и обновили количество апельсинов.

Копирование словаря

В случае списков мы можем сложить два списка двумя способами:

  • Просто сложить с помощью оператора + два списка и получить новый
  • Сделать копию одного списка и дополнить ее данными из второго

Но словари нельзя складывать, да и срезы словари тоже не поддерживают. Зато у словаря есть метод copy. Он работает как копирование списка с помощью среза [:] — при вызове он возвращает поверхностную копию из словаря. Так же ее называют «неглубокой копией» или shallow copy. Поверхностная копия воспроизводит только структуру словаря: не копирует значения, а только создает на них новые ссылки. Тем не менее поверхностная копия — это новый словарь, который может изменять свой состав, не влияя на оригинал:

d = {"a": 1, "b": [2]}
c = d.copy()
c.update({"a": 10, "1k": 1000})
c  # {'a': 10, 'b': [2], '1k': 1000}
c["b"].append(None)
c  # {'a': 10, 'b': [2, None], '1k': 1000}
d  # {'a': 1, 'b': [2, None]}

Словарь c получил собственную структуру, при этом его обновление не затронуло оригинальный словарь d. Однако изменение объекта списка по ссылке затронуло и оригинал, потому что при копировании словаря ссылка на список тоже "скопировалась".

Очистка словаря

Списки можно очистить с помощью присваивания срезу my_list[:] = . В случае словаря вместо присваивания срезу используется метод clear.

Метод clear() удаляет все элементы из текущего словаря:

d = {"a": 1}
d.clear()
print(d)  # {}

Инициализация новых значений

Представьте ситуацию: вам нужно хранить в словаре в качестве значений списки или любые другие изменяемые данные. У вас есть ключ и элемент для добавления в список-значение, но сам ключ в словаре может быть не представлен. В таком случае придется писать подобный код:

if key not in dictionary:
    dictionary[key] = []  # инициализируем список
dictionary[key].append(value)  # изменяем список

Подобная ситуация встречается не так уж и редко. Это понимали и авторы стандартной библиотеки Python и дали словарю метод setdefault.

Инициализация новых значений

Попробуем переписать код выше с помощью метода setdefault:

dictionary.setdefault(key, []).append(value)

Метод setdefault принимает ключ и значение по умолчанию, а затем возвращает ссылку на значение в словаре, связанное с указанным ключом. Если ключ в словаре отсутствует, то метод помещает по ключу, то самое значение по умолчанию и возвращает ссылку на него. В примере выше значением по умолчанию выступает пустой список [].

Тип defaultdict

В стандартной поставке Python присутствует модуль collections, который предоставляет тип defaultdict. Во всех отношениях defaultdict — это обычный словарь. При этом у него есть одно уникальное свойство: там, где обычный словарь "ругается" на отсутствие ключа, defaultdict сам возвращает значение по умолчанию. Рассмотрим пример:

from collections import defaultdict
d = defaultdict(int)
d["a"] += 1
d["b"] = d["c"] + 5
print(d)  # defaultdict(<class 'int'>, {'a': 1, 'c': 0, 'b': 5})

При создании словаря мы указали в качестве аргумента функцию int. Если эту функцию вызвать без аргументов, то она вернет 0. Именно этот вызов внутри словаря d и происходит всякий раз, когда нужно получить значение для несуществующего ключа.

В примере выше d["a"] += 1 дает 1, потому что этот код работает так:

  • Сначала для ключа "a" создается начальное значение — делается вызов int() и получается 0
  • Уже потом к нему прибавляется 1

В строчке d["b"] = d["c"] + 5 создаются значения для ключей "b" и "c". Затем уже по ключу "b" записывается сумма 0 + 5. Вот еще один пример — на этот раз с самодельной функцией-инициализатором:

from collections import defaultdict

def new_value():
    return "1"

x = defaultdict(new_value)
x[1]  # '1'
x["2"]  # '1'
print(x)  # defaultdict(<function new_value at 0x0000018652C1E2A0>, {1: '1', '2': '1'})

Попробуем отбросить немного непонятное упоминание функции-инициализатора. Так станет видно, что теперь строки '1' записаны по всем ключам, по которым мы обращались к содержимому словаря.

Отличия defaultdict от обычного словаря с setdefault

Пока не совсем понятно, зачем иметь оба способа, если они настолько похожи. Но давайте сравним эти две строки:

a.setdefault(key, []).append…

и

b[key].append…

b — это defaultdict(list).

Строки очень похожи, но есть одно различие:

  • В первом случае объект пустого списка будет создаваться каждый раз
  • Во втором случае новый список создается только тогда, когда ключ не будет найден

Значения аргументов всегда вычисляются до того, как будет вызвана функция. Поэтому здесь в случае с setdefault(key, []) затратами на создание пустого списка можно пренебречь. Если вдруг затраты на создание значения по умолчанию окажутся велики, вариант с defaultdict окажется гораздо предпочтительнее.

Зачем вообще использовать setdefault? Он помогает инициализировать разные значения по разным ключам. Значение по умолчанию передается каждый раз, поэтому мы можем хранить по разным ключам даже разные типы данных. С defaultdict у нас нет контроля над тем, какие значения по каким ключам класть. Функция-инициализатор вызывается каждый раз одна и та же — ключ в нее не передается. Наконец, всегда остаются редкие случаи, когда и defaultdict не подходит. Например, если нужно инициализировать значения по-разному, но не подходит и setdefault. Новые значения неизменяемы, их не получится изменить по возвращаемой ссылке.

Попробуйте сами запустить код в окне ниже с интерпретатором Python и повторите примеры из статьи чтобы самим увидеть и понять как всё это работает. Для этого в ячейке с кодом нажмите клавиши на клавиатуре Shift+Enter или запустите код через кнопку Run по значку ▶.