Перейти к содержанию

Словари

Что такое dict

dict (от английского «dictionary», словарь) – еще один тип данных в Python. Словари хранят пары ключ: значение. То есть в списках можно достать элемент, если указать его позицию в виде целого числа, а в словарях – тот самый ключ. dictнеупорядоченный тип данных, поэтому достать элемент по номеру не получится, но отображение содержимого будет в порядке добавления элементов. Уникальность ключей должна поддерживаться, чтобы всегда можно было быстро найти одно единственно верное значение.

В некоторых языках программирования можно встретить ассоциативные массивы – полную аналогию dict. Также вспомним базы данных, широко использующиеся во всём мире для хранения информации: в таблице можно установить первичный ключ, который уникально идентифицирует запись, как и ключ соответствует значению в словаре. Самый простой пример ключа - порядковый номер объекта!

Создание словаря

Использовать словарь стоит, когда нужно сохранять объекты с какими-то ключами и обращаться к объектам по известным ключам. Один из способов определения словаря: указание пар ключ: значение через^запятую внутри фигурных скобок {}. Напоминает set, правда? {} позволяет создать пустой словарь, но не пустое множество.

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

1
2
3
# локально попробуйте поменять значение переменной dates
dates = {"Кунг Фьюри": "1968-09-09", "Наташа Романова": "1985-03-15"}
print(dates)
{'Кунг Фьюри': '1968-09-09', 'Наташа Романова': '1985-03-15'}

В примере dates имеет две пары значений. В первой паре строка "Кунг Фьюри" является ключом, а "1968-09-09" – его значением.

Получение значения по ключу

Чтобы получить значение по ключу, необходимо обратиться к переменной, содержащей словарь, и указать ключ в квадратных скобках []:

dates["Кунг Фьюри"]
'1968-09-09'

Если указать неверный ключ в [], Python будет ругаться: выбросит ошибку KeyError и перестанет выполнять код. Чуть ниже посмотрим, как можно избежать таких ситуаций.

1
2
3
4
5
6
7
# пока этого ключа нет в словаре, будет ошибка при обращении
# поэтому используем перехват ошибок
try:
    print(dates["Капитан Ямайка"])

except KeyError as e:
    print(f"Ключа действительно нет: {e}")
Ключа действительно нет: 'Капитан Ямайка'

Изменение и добавление значений

Синтаксис изменения значения по ключу и добавления нового ключа со значением одинаковый: в [] нужно указать ключ, поставить = и указать значение, которое теперь будет соответствовать ключу.

1
2
3
4
5
6
7
# этот ключ уже был в примере
dates["Кунг Фьюри"] = "1960-09-09"

# а такого не было
dates["Капитан Ямайка"] = "1930-10-04"

print(dates)
{'Кунг Фьюри': '1960-09-09', 'Наташа Романова': '1985-03-15', 'Капитан Ямайка': '1930-10-04'}

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

Основные методы словаря

Проверка вхождения и get()

Помните, ранее говорили, что обращение к несуществующему ключу приводит к ошибке? Пришло время посмотреть пару способов борьбы!

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

1
2
3
4
5
# еще способ создания: пары можно передавать как аргументы dict через `=`
marks = dict(линал=100, английский=92)

print(f'{"матан" in marks = }')
print(f'{"линал" in marks = }')
"матан" in marks = False
"линал" in marks = True

В коде проверку можно использовать в условной конструкции if, чтобы принимать решение в зависимости от наличия ключа:

1
2
3
4
5
if "матан" in marks:
    print(marks["матан"])

else:
    print("Нет оценки по матану :(")
Нет оценки по матану :(

Теперь о методе get(): при помощи его тоже можно получать значения из словаря по ключу. KeyError никогда не появится: если ключа нет, по умолчанию возвращается None:

1
2
3
empty_dict = {}

print(empty_dict.get("ключ"))
None

Вторым аргументом метода get() можно указать значение, которое должно возвращаться вместо None, когда ключ не был найден:

print(empty_dict.get("ключ", -1))
-1

Что такое "длина словаря"?

Функция len() для словаря будет возвращать количество пар ключ: значение (их столько же, сколько ключей), которое в нём содержится:

1
2
3
4
5
# empty_dict – пустой словарь, поэтому длина равна 0
print(f"{len(empty_dict) = }")

# а вот словарь marks уже содержит две пары, поэтому длина 2
print(f"{len(marks) = }")
len(empty_dict) = 0
len(marks) = 2

Удаление из словаря

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

  • при помощи инструкции del (от английского «delete») можно удалить пару ключ: значение (удаление ключа эквивалентно удалению пары ключ: значение, так как теряем возможность найти то самое значение), в общем виде:

    1
    2
    3
    # таким образом из словаря "словарь" будет удален ключ "название_ключа"
    # и соответствующее ему значение
    del словарь[название_ключа]
    

    Предположим, коллега из самого первого примера уволился и больше нет смысла хранить его день рождения:

    1
    2
    3
    4
    # из словаря dates удаляется ключ "Наташа Романова"
    del dates["Наташа Романова"]
    
    print(dates.get("Наташа Романова"))
    
    None
    

  • pop() - метод, который достает значение, хранящееся по переданному ключу, и сразу удаляет ключ из словаря:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # ещё один способ создания словаря из последовательности пар
    holidays = dict([("January", [1, 2, 3, 4]), ("Feburary", [23]), ("March", [8])])
    
    # pop() возвращает значение, соответствующее ключу, значит его можно присвоить переменной
    january_days = holidays.pop("January")
    
    # напечатается соответствующий массив
    print(f"{january_days = }")
    print(f"{holidays = }")
    
    january_days = [1, 2, 3, 4]
    holidays = {'Feburary': [23], 'March': [8]}
    

    Для метода pop() есть возможность указать значение, которое будет возвращено при обращении к несуществующему ключу. Почти как get(), но всё-таки, без указания этого значения, pop() выбрасывает KeyError.

  • popitem() имеет схожее название, но не путайте с предыдущим методом: этот на вход не принимает ключ, а возвращает пару ключ: значение, которая была добавлена последней (такое поведение гарантируется с Python 3.7).

    1
    2
    3
    # в результате – последняя добавленная пара
    print(f"{holidays.popitem() = }")
    print(f"{holidays = }")
    
    holidays.popitem() = ('March', [8])
    holidays = {'Feburary': [23]}
    

  • clear() позволяет удалить сразу все ключи словаря, то есть полностью его очистить:

    1
    2
    3
    4
    5
    6
    # вернёмся к предыдущему примеру
    # словарь становится пустой
    holidays.clear()
    
    # значит, длина равна 0
    print(f"{len(holidays) = }")
    
    len(holidays) = 0
    

Важно

Обратите внимание на то, как работают методы pop(), popitem() и clear(): как только вызываются, словарь меняет свой состав (изменения происходят in place, то изменить по месту без копирования).

Обновление и добавление ключей

Лицезрели, что значения в словарь можно добавлять или менять, обращаясь по ключу. Python предоставляет возможность не писать кучу присваиваний, а использовать лаконичный метод update(), который на вход может принимать либо другой словарь, либо пары ключ: значение в какой-то последовательности (например, кортежи по два значения в списке: первое – ключ, второе – значение)

# создадим два словаря: в первом уже есть два ключа
quidditch_team = {"Fred Weasley": "3rd year", "George Weasley": "3rd year"}

# во втором – один ключ
new_members = {"Harry Potter": "1st year"}

# добавим пары из new_members
# метод update() также работает in place, поэтому после выполнения данной
# строки кода, в словаре quidditch_team станет три ключа
quidditch_team.update(new_members)

print(quidditch_team["Harry Potter"])
1st year

А что, если в update() передать пары, ключ которых уже был в словаре? Значения по дублирующимся ключам будут перезаписаны на новые:

1
2
3
4
5
6
7
# данный ключ (то, что записано первым в кортеже) уже есть в quidditch_team
member_update = [("Harry Potter", "2nd year")]

# значение, соответствующее "Harry Potter", будет переписано
quidditch_team.update(member_update)

print(quidditch_team["Harry Potter"])
2nd year

Доступ к ключам и значениям

В Python можно без проблем извлекать отдельно по ключам или значениям, а также итерироваться по элементам словарей в цикле for. Осталось разобраться, как это работает.

Ключи

По умолчанию, в конструкциях вида

1
2
3
# после in указано название переменной, хранящей словарь
for key in dict_var:
    ...

переменные цикла (тут – key) будут принимать значения из множества ключей словаря. Аналогично можно использовать метод keys() (позволяет достать все ключи), который явно говорит, что цикл идет по ключам, например:

1
2
3
4
5
# словарь в качестве ключей хранит имена игроков
for player in quidditch_team:

    # на каждой итерации будет напечатан ключ и значение
    print(f"{player = }: {quidditch_team[player] = }")
player = 'Fred Weasley': quidditch_team[player] = '3rd year'
player = 'George Weasley': quidditch_team[player] = '3rd year'
player = 'Harry Potter': quidditch_team[player] = '2nd year'

Значения

При помощи метода values() можно получить все значения, хранящиеся по всем ключам словаря:

1
2
3
4
5
# словарь в качестве значений хранит годы обучения
for year in quidditch_team.values():

    # на каждой итерации будет год обучения игрока
    print(year)
3rd year
3rd year
2nd year

Между тем

Напрямую по значению получить ключ нельзя.

Всё и сразу

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

1
2
3
4
5
6
7
# сразу две переменные: первая последовательно будет ключами,
# вторая – значениями
for player, year in quidditch_team.items():

    # items() избавляет от необходимости обращаться quidditch_team[player],
    # чтобы получить значение. Оно уже в year
    print(f"Player {player} is in {year}")
Player Fred Weasley is in 3rd year
Player George Weasley is in 3rd year
Player Harry Potter is in 2nd year

Сортировка

Функция sorted() доступна и для словарей. По умолчанию ключи словаря поддерживают порядок, в котором были добавлены, но можно отсортировать их в нужном направлении (в зависимости от типа):

1
2
3
4
5
6
# вспомним про рабочие дни, ключи - целые числа по номеру дня недели
week = {7: "weekend", 6: "weekend", 1: "workday"}

# в sorted_week окажутся ключи, отсортированные в порядке возрастания
sorted_week = sorted(week)
print(f"Порядок возрастания: {sorted_week}")
Порядок возрастания: [1, 6, 7]

Не забудьте

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

Можно ли отсортировать словарь по значениям?

Да, можно попробовать самостоятельно разобраться с аргументами функции sorted().

Что можно хранить

Теперь добавим немного технических подробностей: возможно, уже заметили самостоятельно, что dict может принимать в качестве ключа не всякое значение. На самом деле только хешируемые объекты (можно вызвать функцию hash() и получить значение) могут быть ключами словаря, на значения это ограничение не распространяется. В dict и set значение хеша от объекта используется для поиска внутри структуры.

Ключом словаря нельзя сделать объект изменяемого типа, например, list, set или сам dict, так как значение их хеша может измениться со временем. Неизменяемый кортеж может быть ключом только если не содержит внутри изменяемые объекты.

Изменяемость и неизменяемость

В англоязычной литературе изменяемые типы называют mutable, а неизменяемые – immutable, почитать документацию.

В Python всё есть объект. Когда пользователь присваивает значение переменной, она начинает ассоциироваться с ячейкой памяти, где лежит это значение. Переменная знает адрес, откуда можно получить значение. id() и hex() показывают адрес в памяти компьютера. id() - адрес в десятичном виде, а hex() поможет перевести в шестнадцатеричный.

По адресу лежит так называемое внутреннее состояние переменной:

  • неизменяемые типы не позволяют менять внутреннее состояние, значение переменной может поменяться только вместе с адресом
  • изменяемые типы позволяют менять внутреннее состояние переменной при сохранении адреса (возвращаемое id() значение не меняется, но значение переменной каким-то образом преобразовывается). Изменение по ссылке называется изменением in place.

Неизменяемые типы

Из стандартных неизменяемыми являются:

  • int
  • float
  • bool
  • str
  • tuple

Давайте сразу рассмотрим пример:

1
2
3
4
counter = 100

# полученное вами значение адреса может отличаться
print(f"{counter = }, {hex(id(counter)) = }")
counter = 100, hex(id(counter)) = '0x10c678d50'

flowchart LR

    subgraph 100
       0x10c678d50
    end

    counter --> 100

А теперь поменяем значение counter:

counter = 200
print(f"{counter = }, {hex(id(counter)) = }")
counter = 200, hex(id(counter)) = '0x10c6799d0'

Кажется, что раз значение переменной counter поменялось, то и содержимое по предыдущему адресу изменилось? Нет, на самом деле counter теперь указывает в другое место:

flowchart LR

    subgraph 100
       0x10c678d50
    end

    subgraph 200
       0x10c6799d0
    end

    counter --> 200

Из интересного: Python заранее создает объекты для чисел от -5 до 256, поэтому для переменных со значением из этого диапазона берутся заранее готовые ссылки.

1
2
3
4
5
6
7
# создадим две переменные с одинаковыми значениями в диапазоне от -5 до 256
a = 20
b = 20

# a и b указывают на одно и то же место в памяти
# попробуйте у себя поменять значение a и b на число больше 256 или меньше -5
id(a) == id(b)
True

Изменяемые типы

Стандартные изменяемые типы это:

  • list
  • set
  • dict

У списков есть метод append(), позволяющий добавить в него значение:

1
2
3
4
5
6
# создадим список и напечатаем его адрес
ratings = [1, 2, 3]
print(f"Было: {hex(id(ratings))}")

ratings.append(4)
print(f"Стало: {hex(id(ratings))} - ничего не поменялось!")
Было: 0x10df8d1c0
Стало: 0x10df8d1c0 - ничего не поменялось!

flowchart LR

    subgraph list ["[1, 2, 3]"]
       0x10df8d1c0
    end

    ratings --> list

После добавления ещё одного, адрес ratings не изменился.

flowchart LR

    subgraph list ["[1, 2, 3, 4]"]
       0x10df8d1c0
    end

    ratings --> list

Что узнали из лекции

  • новый тип данных – словарь! Позволяет хранить соответствие ключ: значение;
  • несколько способов создания dict, примеры:

    # при помощи "литерала" - фигурных скобок {}
    flowers = {"roses": "red", "violets": "blue"}
    
    # при помощи вызова
    #   dict() и последовательности с парами значений
    anime = dict(
        [
            ("Ведьмак", "Кошмар волка"),
            ("Призрак в доспехах", ["Призрак в доспехах", "Синдром одиночки", "Невинность"])
        ]
    )
    
    #   dict() и "ключ=значение"
    literature = dict(poem_flowers=flowers)
    
    print(f"{flowers = }\n{anime = }\n{literature = }")
    
    flowers = {'roses': 'red', 'violets': 'blue'}
    anime = {'Ведьмак': 'Кошмар волка', 'Призрак в доспехах': ['Призрак в доспехах', 'Синдром одиночки', 'Невинность']}
    literature = {'poem_flowers': {'roses': 'red', 'violets': 'blue'}}
    

  • методы для изменения состояния или получения доступа к элементам:

    1
    2
    3
    4
    5
    # доступ к элементу, если ключа нет - ошибка
    print(f'{flowers["violets"] = }')
    
    # при помощи get()
    print(f'{flowers.get("magnolias") = }')
    
    flowers["violets"] = 'blue'
    flowers.get("magnolias") = None
    

    days = ["Пн", "Вт", "Ср", "Чт", "Пт"]
    
    # создадим пустой словарь
    numbered_days = {}
    
    # будем добавлять в него элементы в цикле
    for num in range(len(days)):
        numbered_days[num] = days[num]
    
    # получим отдельно ключи и значения
    # пары из tuple можно сразу получить при помощи метода items()
    keys = numbered_days.keys()
    values = numbered_days.values()
    
    print(f"Ключи: {keys}\nЗначения: {values}")
    
    Ключи: dict_keys([0, 1, 2, 3, 4])
    Значения: dict_values(['Пн', 'Вт', 'Ср', 'Чт', 'Пт'])
    

  • требование к ключу: возможность хеширования, свойство ключа внутри словаря: уникальность;

  • разобрали, что есть изменяемые (dict, set, list) и неизменяемые (int, float, bool, tuple, str) стандартные типы данных.