Кортежи и словари

Кортежи

Кроме типа list, применяющегося для работы со списками, в Python существует тип данных “кортеж”, tuple. Кортежи работают точно так же, как и списки, за исключением того, что ссылки в ячейках кортежа нельзя изменять.

Примеры создания кортежей с помощью типа tuple:

t = tuple()
t = tuple([10, 20, 30])  # преобразование списка в кортеж

Кроме того, для кортежей есть свои литералы:

t = (10, 20, 30)  # Кортеж из трёх элементов.
t = ()     # Пустой кортеж.
t = (10,)   # Кортеж из одного элемента, запятая обязательна.

Обратите внимание на то, что в кортеже из одного элемента обязательно нужно указывать запятую. Так происходит потому, что иначе невозможно отличить литерал кортежа от оператора (), повышающего приоритет операции. То есть (10) – это просто число 10 в скобках, то есть число. А (10,) – это уже кортеж из одного элемента, числа 10.

Из кортежей можно извлекать значения по индексам, а так же применять к ним те же операции и методы, что и к спискам, за исключением тех, которые изменяют список.

t = (10, 20, 30)
print(t[0])  # 10
print(t.index(30))  # 2
t[0] = 1000   # Ошибка, кортеж неизменяем.

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

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

t = ([10, 20], [100, 200])
t[0].append(30)   # выполнится, вложенные объекты изменять можно
print(t)  # ([10, 20, 30], [100, 200])
t[1] = 123        # Ошибка. Ссылки в ячейках кортежа изменять нельзя.

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

Словари

Словарь (в других языках: хеш-таблица, ассоциативный массив) – множество пар вида “ключ-значение”, позволяющее производить поиск значения по ключу за постоянное, не зависящее от размера словаря время. При этом, каждый ключ должен быть уникален, то есть не может хранится двух пар с одинаковыми ключами.

Словари в Python являются объектами типа dict. Создать пустой словарь можно одним из двух следующих способов:

d = {}  # Пустой словарь

d = dict()  # Тоже пустой словарь

В Python ключами словаря могут быть только неизменяемые типы данных (строки, числа, неизменяемые множества и кортежи, содержащие значения неизменяемых типов). Так же подходят комбинации этих типов, вложенные друг в друга.

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

d = {}
d['John'] = 1000
d['Mary'] = 1500
d['Vasya'] = 2000
print(d)  # при запуске этого примера автором лекций, было выведено
          # {'Mary': 1500, 'Vasya': 2000, 'John': 1000}
          # Но в вашем случае порядок пар может быть другим.

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

Кроме того, если посмотреть на то, как выводится словарь в примере выше, можно увидеть литерал словаря, который можно использовать для того, чтобы задать словарь из нескольких элементов прямо в коде программы. Литерал словаря состоит из пар ключ-значение, записанных через двоеточие и разделённых запятыми. Весь литерал заключается в фигурные скобки.

d = {'John': 1000, 'Mary': 1500, 'Vasya': 2000}

Словарь поддерживает следующие операции:

  • поиск по ключу

    d[key]
    

    Если пары с таким ключом в словаре нет, эта операция приведёт к ошибке.

  • создание новой пары “ключ-значение”. Если пары с таким ключом не существует, она будет добавлена. Если же пара с указанным ключом уже есть, старое значение пары будет заменено на новое.

    d[key] = value
    
  • удаление пары по ключу

    value = d.pop(key)  # pop одновременно удаляет пару ключ-значение
                        # и возвращает значение
    
    # альтернативный способ
    del d[key]
    
  • получение множества ключей

    d.keys()  # Итератор по ключам
    

    Итератор предназначен для однократного прохода в цикле for, если хотите обращаться к ключам по индексу, их нужно преобразовать в список:

    key_list = list(d.keys())  # Список ключей
    
  • получение множества значений

    values = d.values()  # Итератор
    
    value_list = list(d.values())  # Список
    
  • получение множества пар

    values = d.items()  # итератор по кортежам с парами (ключ, значение)
    
    value_list = list(d.items())  # список корежей (ключ, значение)
    

Пройтись по ключам массива можно следующим образом:

for key in d.keys():  # Можно указать итератор по ключам явно
    print(key, '=>', d[key])

for key in d:  # Но по умолчанию цикл for так же будет перебирать ключи
    print(key, '=>', d[key])

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

for key, value in d.items():
    print(key, '=>', value)

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

d = {'Mary':1500, 'Vasya': 2000, 'John': 1000}
'Mary' in d  # True
'Vincent' in d  # False

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

d.clear()

Вычислительная сложность

Вычислительная сложность операций со словарём в обозначениях O(f(N)) приведена в следующей таблице:

Операция сложность в среднем сложность в худшем случае
d.copy() O(N) O(N)
d[key] O(1) O(N)
d[key] = value O(1) O(N)
d.pop(key) O(1) O(N)
for i in d: O(N) O(N)

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

Тип OrderedDict

Как уже много раз упоминалось в лекции, порядок обхода пар в словаре не определён и может сильно меняться при добавлении или удалении пары из словаря. Но иногда хотелось бы запомнить порядок добавления пар. Для этого существует тип данных OrderedDict из модуля collections:

import collections

d = collections.OrderedDict()
# Далее можно работать с d как со словарём.

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