Использование словарей
Синтаксис
Как и другие встроенные коллекции, словари поддерживаются языком и имеют собственный синтаксис для описания литералов.
Литерал словаря записывается в фигурных скобках. Пары «ключ-значение» разделяются запятыми, а ключ отделяется от значения двоеточием:
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 по значку ▶.