# Глава 15. Словари
Рассмотрим тип `dict` (dictionary, словарь) для работы с отображенями ключ-значение. Важно, что `dict` незримо присутствует абсолютно в любом python-коде: он прочно вшит в низкоуровневое представление данных.
Все объекты, будь то целые числа, функции или кортежи, имеют id, тип и другие атрибуты. А это не что иное, как пары ключ-значение внутри словаря! Неудивительно, что класс `dict` разрабатывался с упором на эффективность.
## Создание словарей
Класс `dict` — это коллекция для хранения пар: уникальных ключей, к которым привязаны значения. Примеры таких пар: трек-номер посылки и адрес доставки, id пользователя и его телефон.
Ключом словаря может выступать любой [хешируемый объект.](/courses/python/chapters/python_chapter_0140#block-hash) Сам словарь является изменяемой коллекцией с возможностью удаления и добавления элементов. Как и множество, словарь реализован на базе [хеш-таблицы.](https://ru.wikipedia.org/wiki/%D0%A5%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D0%B0) Поэтому [алгоритмическая сложность](https://ru.wikipedia.org/wiki/%D0%92%D1%8B%D1%87%D0%B8%D1%81%D0%BB%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F_%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C) поиска, добавления и удаления элементов в `dict` оценивается как амортизированная константа O(1).
Значением словаря, привязанным к ключу, может быть абсолютно любой объект: список, множество, `None`. Можно создавать структуры с большой вложенностью: словари, содержащие другие словари, содержащие списки.
Инициализируется словарь `dict` двумя способами: через литерал `{}` и конструктор `dict()`. Мы уже [обсудили,](/courses/python/chapters/python_chapter_0140/#block-create-set) что посредством фигурных скобок инициализируются не только словари, но и множества. Элементы множества перечисляются в скобках через запятую. А при заведении словаря перечисляются пары `key: value`.
```python {.example_for_playground}
# Ключ словаря - id пользователя. Значение - логин пользователя.
users = {90278: "pasha", 136743: "slumbering.eoraptor"}
print(type(users))
print(len(users))
```
```
<class 'dict'>
2
```
В данном примере через литерал `{}` был создан словарь из двух элементов. А вот как выглядит вариант создания словаря с тем же содержимым через конструктор `dict()`:
```python
users = dict([
(90278, "tanya"),
(136743, "slumbering.eoraptor")
])
```
Конструктор `dict()` ожидает аргумент, по которому можно итерироваться для формирования пар ключ-значение. В примере мы использовали список кортежей. С тем же успехом мы могли передать и список списков или кортеж кортежей. Иными словами, итерабельный объект, содержащий итерабельные объекты.
Перечислите через пробел ключи словаря `d` в любом порядке. В случае ошибки напишите `error`. {.task_text}
```python {.example_for_playground}
lst = ["or", "to", "an"]
d = dict(lst)
```
```consoleoutput {.task_source #python_chapter_0150_task_0003}
```
Конструктор `dict()` итерируется по списку `lst` для составления пар: ключей и значений нового словаря. По строкам можно итерироваться точно так же, как и по спискам. Поэтому первая буква строки превращается в ключ, а вторая в значение словаря. В итоге `d` содержит три записи: `{'o': 'r', 't': 'o', 'a': 'n'}`. Ключами являются буквы `o`, `t`, `a`. {.task_hint}
```python {.task_answer}
ota
```
Если ключи словаря — строки, то возможны два варианта инициализации. Первый — интуитивный: обернуть строки в кавычки.
```python {.example_for_playground}
# Ключ словаря - код товара. Значение - количество единиц товара в магазине.
products = dict([
("T3234FX", 12),
("T048Y11", 0),
("QW23302", 9)
])
print(products)
```
```
{'T3234FX': 12, 'T048Y11': 0, 'QW23302': 9}
```
Второй способ — избавиться от кавычек и работать с парами ключ-значение как с [именованными аргументами](/courses/python/chapters/python_chapter_0060#block-pos-named-args) функции `dict()`:
```python {.example_for_playground}
products = dict(
T3234FX=12,
T048Y11=0,
QW23302=9
)
print(products)
```
```
{'T3234FX': 12, 'T048Y11': 0, 'QW23302': 9}
```
Этот способ сработает, только если ключ-строка удовлетворяет всем требованиям к именованию переменной в питоне (например, не начинается с цифры, не содержит тире и спецсимволы).
## Операции над словарями
Синтаксис квадратных скобок `[]`, в которых указывается ключ, используется для:
- поиска значения в словаре по ключу,
- записи и перезаписи значения по ключу.
В этом примере мы заводим словарь `cities`, в котором хранятся названия городов и их координаты (широта и долгота). Заполняем словарь данными о двух городах, а затем извлекаем значение по ключу. Встроенная функция `len()` нужна, чтобы определить размер словаря (количество пар ключ-значение).
```python {.example_for_playground}
# Заводим пустой словарь
cities = {}
# Добавляем в словарь значения
cities["Tokyo"] = (35.6817, 139.7539)
cities["Cairo"] = (30.0505, 31.2464)
# Ищем значения по ключу
cairo_coordinates = cities["Cairo"]
print(f"Cairo is located here: {cairo_coordinates}")
n = len(cities)
print(f"We have {n} cities")
```
```
Cairo is located here: (30.0505, 31.2464)
We have 2 cities
```
Оператор `in` используется для проверки, содержится ли в словаре интересующий ключ.
```python {.example_for_playground}
cities = {"Tokyo": (35.6817, 139.7539), "Cairo": (30.0505, 31.2464)}
print("Tokyo" in cities)
print("Palermo" in cities)
print(cities["Tokyo"])
print(cities["Palermo"])
```
```
True
False
(35.6817, 139.7539)
Traceback (most recent call last):
File "/home/ail/test.py", line 7, in <module>
print(cities["Palermo"])
~~~~~~^^^^^^^^^^^
KeyError: 'Palermo'
```
Для существующего ключа `"Tokyo"` оператор `in` вернул `True`, и посредством `[]` было получено его значение. Для не существующего ключа `"Palermo"` `in` вернул `False`, а обращение по этому ключу привело к исключению `KeyError`.
Подчеркнем, что квадратные скобки позволяют не только прочитать значение, но и модифицировать существующее или сохранить новое:
```python {.example_for_playground}
# Заводим словрь, содержащий авторов и их книги
authors = {"Luciano Ramalho": "Fluent Python"}
# Изменяем название книги
authors["Luciano Ramalho"] = "Fluent Python. Clear, concise, and Effective programming"
# Добавляем нового автора
authors["Jason Brownlee"] = "Python Asyncio Jump-Start"
print(authors)
```
```
{'Luciano Ramalho': 'Fluent Python. Clear, concise, and Effective programming', 'Jason Brownlee': 'Python Asyncio Jump-Start'}
```
В этом примере мы изменили значение по ключу `"Luciano Ramalho"` и добавили новое по ключу `"Jason Brownlee"`.
Что выведет этот код? В случае ошибки напишите `error`. {.task_text}
```python {.example_for_playground}
f = lambda x: x % 10
d = dict()
for val in [15, 20, 30, 31]:
k = f(val)
d[k] = val
print(len(d))
```
```consoleoutput {.task_source #python_chapter_0150_task_0005}
```
Мы объявили лямбда-функцию `f`, которая принимает число `x` и с помощью оператора `%` возвращает остаток от деления `x` на 10. Затем в цикле мы применяем эту лямбду к числам. Отаток от деления этих чисел на 10 становится ключом в словаре `d`. Мы получили остаток от деления 15 на 10. Это 5. По ключу 5 записали значение 15. Остаток от деления чисел 20 и 30 совпадает и равен нулю. Поэтому вначале по ключу 0 мы записали значение 20, а затем перезаписали его значением 30. Остаток от деления 31 на 10 равен 1. Таким образом, в словаре `d` накопилось 3 элемента. {.task_hint}
```python {.task_answer}
3
```
Как считаете, что не так с этим кодом?
```python {.example_for_playground}
d = {3: "triangle", 4: "square", 6: "hexagon"}
x = d[3:4]
```
На строке 2 произведена попытка обращения к элементам словаря через [срез,](/courses/python/chapters/python_chapter_0100##block-slices) а не по ключу. Так как для типов `dict` и `set` операция получения среза бессмысленна, генерируется исключение:
```
Traceback (most recent call last):
File "example.py", line 2, in <module>
x = d[3:4]
~^^^^^
TypeError: unhashable type: 'slice'
```
Напишите функцию `to_dict(items)`, которая в качестве аргумента принимает некоторый итерабельный объект (например, список или кортеж). Функция должна создать на базе него словарь и вернуть его. Ключ словаря — элемент `items`, а значение — индекс этого элемента в последовательности `items` (индексация с нуля). {.task_text}
Если элемент встречается в последовательности несколько раз, нужно сохранить индекс **последнего** вхождения. {.task_text}
Если в `items` попадутся не хешируемые объекты, в словарь их добавлять не надо. {.task_text}
Например, для входного списка `["first", 2, "third", {4, 44}, "first"]` функция должна вернуть словарь `{'first': 4, 2: 1, 'third': 2}`. {.task_text}
```python {.task_source #python_chapter_0150_task_0010}
import typing
def to_dict(items):
# Your code here
```
Создайте пустой словарь. Проитерируйтесь по `items` и заполните словарь хэшируемыми элементами `items` и их индексами. Для проверки, является ли объект `item` хешируемым, используйте функцию `isinstance(item, typing.Hashable)`. Она объявлена в модуле `typing`. {.task_hint}
```python {.task_answer}
import typing
def to_dict(items):
d = {}
for i, item in enumerate(items):
if isinstance(item, typing.Hashable):
d[item] = i
return d
```
Удалить из словаря значение можно с помощью оператора `del`:
```python {.example_for_playground}
d = {3: "triangle", 4: "square", 6: "hexagon"}
del d[4]
print(d)
del d[4]
```
```
{3: 'triangle', 6: 'hexagon'}
Traceback (most recent call last):
File "/home/ail/test.py", line 6, in <module>
del d[4]
~^^^
KeyError: 4
```
Обратите внимание: при попытке удаления элемента по уже не существующему ключу оператор `del` генерирует исключение `KeyError`.
Выведите на экран значение 1024 из вложенного объекта `obj`.{.task_text}
```python {.task_source #python_chapter_0150_task_0020}
obj = [
"sort",
45,
(16, 17),
{
1: 3,
"k2": None,
"k6": {
"val": 1024
}
}
]
```
Выражение для доступа к нужному значению: `obj[3]["k6"]["val"]` {.task_hint}
```python {.task_answer}
obj = [
"sort",
45,
(16, 17),
{
1: 3,
"k2": None,
"k6": {
"val": 1024
}
}
]
print(obj[3]["k6"]["val"])
```
Обойти в цикле словарь можно двумя способами. Первый: перебрать ключи и по ним получить значения:
```python
for k in d:
print(k, d[k])
```
Второй способ: вызвать метод `items()` для итерации по парам ключ-значение:
```python
for k, v in d.items():
print(k, v)
```
Подытожим основные действия над словарями: {#block-basic-ops}
- `len(d)` — определение размера словаря.
- `k in d`, `k not in d` — проверка, содержится или нет в словаре нужный ключ.
- `d[k]` — обращение к значению в словаре по ключу. Используется для получения, изменения или добавления значения.
- `del d[k]` — удаление пары ключ-значение из словаря.
- `d.items()` — метод для итерации по парам ключ-значение.
Создайте функцию `calc_visits(users)`, аргумент которой — список id пользователей, посетивших страницу некоего сайта. Причем id могут повторяться. Необходимо посчитать количество посещений для каждого из пользователей: функция должна вернуть словарь, ключ в котором — это id пользователя, а значение — количество посещений. {.task_text}
Например, список [6, 6, 4, 6] трактуется следующим образом: пользователь с id 6 посетил страницу 3 раза, пользователь с id 4 посетил страницу 1 раз.{.task_text}
```python {.task_source #python_chapter_0150_task_0030}
```
Для проверки ключа на вхождение в словарь используйте оператор `in`, для изменения существующего значения по ключу — `[]`. {.task_hint}
```python {.task_answer}
def calc_visits(users):
visits = {}
for user in users:
if user not in visits:
visits[user] = 0
visits[user] += 1
return visits
```
Но на этом популярные операции над типом `dict` не заканчиваются. Также широко применимы методы: {#block-methods}
- `d.copy()` — метод возвращает копию словаря. Например, `d2 = d1.copy()` означает, что изменения, вносимые в словарь `d2`, никак не повлияют на оригинальный словарь `d1`. Если же использовать обычное присваивание `d2 = d1`, то переменная `d2` будет смотреть на тот же самый объект в памяти, что `d1`. И все изменения `d2` отразятся на `d1`!
- `d.clear()` — метод для удаления из словаря всех элементов.
- `d.pop(k, default_val)` — метод для удаления и возврата значения по ключу. Если аргумент `default_val` не задан, а ключ отсутствует в словаре, генерирует исключение `KeyError`.
- `d.get(k, default_val)` — метод для получения значения по ключу. В отличие от `d[k]`, если ключ не найден, не генерирует исключение, а возвращает `default_val`. Этот аргумент опционален и по умолчанию равен `None`: `d.get(k)` для несуществующего ключа вернет `None`.
- `d.update(items)` — метод для обновления содержимого словаря объектами `items`. `items` может быть другим словарем, итерабельным объектом с парами ключ-значение или перечислением именованных аргументов.
- `d.setdefault(k, default_val)` — получить значение `k`, если этот ключ содержится в словаре. Если не содержится, добавить и вернуть значение `default_val` по этому ключу. `default_val` — опциональный аргумент, по умолчанию `None`.
Удалите из словаря значение 5.5. Используйте для этого один из **методов** `dict`, а не оператор `del`.{.task_text}
```python {.task_source #python_chapter_0150_task_0040}
temperatures = {"min": 5.5, "max": 28.0, "avg": 14.1}
```
Воспользуйтесь методом `pop()`. {.task_hint}
```python {.task_answer}
temperatures = {"min": 5.5, "max": 28.0, "avg": 14.1}
temperatures.pop("min")
```
Используем метод `get()` для создания частотного словаря. Частотный словарь — это набор слов в тексте вместе с информацией об их частотности.
```python {.example_for_playground}
def lexicon(text):
# Берем текст и строим по нему частотный словарь
d = {}
for word in text.lower().split():
d[word] = d.get(word, 0) + 1
return d
lex = lexicon("A hash table uses a hash function to compute an index")
for k, v in lex.items():
print(f"'{k}' occurs {v} times in text")
print(f"\nThere are {len(lex)} unique words in text")
# Удаляем артикли
del lex["a"]
del lex["an"]
print(f"There are {len(lex)} unique words in text excluding articles")
```
```
'a' occurs 2 times in text
'hash' occurs 2 times in text
'table' occurs 1 times in text
'uses' occurs 1 times in text
'function' occurs 1 times in text
'to' occurs 1 times in text
'compute' occurs 1 times in text
'an' occurs 1 times in text
'index' occurs 1 times in text
There are 9 unique words in text
There are 7 unique words in text excluding articles
```
Создайте функцию `merge_max(d1, d2)`, принимающую два словаря, ключи и значения в которых — целые числа. Функция должна сформировать из них и вернуть новый словарь. Причем если ключ присутствует в обоих словарях, в новый словарь требуется поместить по этому ключу максимальное значение. {.task_text}
```python {.task_source #python_chapter_0150_task_0050}
```
Для определения максимального из двух значений воспользуйтесь встроенной функцией `max(a, b)`. {.task_hint}
```python {.task_answer}
def merge_max(d1, d2):
merged = {}
for k, v in d1.items():
merged[k] = v
for k, v in d2.items():
if k in merged:
merged[k] = max(merged[k], v)
else:
merged[k] = v
return merged
```
## Является ли dict упорядоченной коллекцией
Начиная с версии питона 3.7 ключи в `dict` гарантированно отсортированы в порядке добавления. До версии 3.7 словарь не был отсортированной коллекцией, и для работы с упорядоченным словарем использовался класс `OrderedDict` из модуля `collections`. Потерял ли `OrderedDict` с тех пор свою актуальность? Не совсем: в нем доступен набор методов, которых нет во встроенном типе `dict`. Подробнее мы рассмотрим `OrderedDict` и другие классы из `collections` в следующих главах.
Несмотря на то, что словари в современном питоне — это упорядоченные коллекции, при сравнении двух словарей оператор `==` вернет `True` вне зависимости от последовательности пар ключ-значение: главное, чтобы они совпадали:
```python {.example_for_playground}
d1 = {1: 1, 2: 2, 3: 3}
d2 = {2: 2, 3: 3, 1: 1}
print(d1 == d2)
```
```
True
```
## Резюмируем
- Тип `dict` (словарь) — это гетерогенная коллекция пар: уникальных ключей, к которым привязаны значения.
- `dict` создается с помощью конструктора `dict()` либо литерала `{}`.
- Начиная с версии питона 3.7 ключи в `dict` упорядочены в порядке добавления.
- Для того чтобы объект мог выступать ключом в `dict`, он должен быть хешируемым.
- Тип `dict` реализован на базе хеш-таблицы. Поэтому алгоритмическая сложность поиска, добавления и удаления элементов у него амортизированное O(1).
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!