dict - словари

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

Вот список, в котором, образно говоря, данные хранятся в виде пар "индекс-значение":

>>> x = [2700, 'manager', 7, 3.14, [1, 0, 2]]
>>>
>>> x
[2700, 'manager', 7, 3.14, [1, 0, 2]]

А вот словарь, в котором хранятся теже самые данные, но уже в виде пар "ключ-значение":

>>> d = {'price': 2700, 'Sam': 'manager', (3, 124): 7, 'pi': 3.14, 'v_1': [1, 0, 2]}
>>>
>>> d
{'price': 2700, 'Sam': 'manager', (3, 124): 7, 'pi': 3.14, 'v_1': [1, 0, 2]}

Проиллюстрировать эти особенности организации данных можно следующим образом:

dict словарь Python

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

  • 'price': 2700 - скорее всего, этот элемент является ценой, ведь не даром ключом элемента является строка 'price' и равна эта цена \(2700\);
  • 'Sam': 'manager' - элемент, который явно показывает что некто Sam является менеджером;
  • (3, 124): 7 - так можно указывать значения элементов разреженной матрицы. Такой ключ (который является кортежем) может указывать местоположение элемента в матрице - строка с индексом \(3\) и столбец с индексом \(127\), а значение элемента равно \(7\);
  • 'pi': 3.14 - имя математической константы и ее значение;
  • 'v_1': [1, 0, 2] - судя по значению, данный элемент скорее всего является вектором, а ключом - обозначение вектора.

Из-за того что ключами словаря могут быть объекты разного типа, то не стоит ожидать от них какого-то внутреннего порядка, который есть в списках. В списках, как и в любой последовательности, есть упорядоченные индексы, а значит мы можем воспользоваться оператором извлечения среза [START:STOP:STEP]:

>>> x = [2700, 'manager', 7, 3.14, [1, 0, 2]]
>>> x
[2700, 'manager', 7, 3.14, [1, 0, 2]]
>>>
>>> x[1:4]
['manager', 7, 3.14]

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

>>> d = {'price': 2700, 'Sam': 'manager', (3, 124): 7, 'pi': 3.14, 'v_1': [1, 0, 2]}
>>> d
{'price': 2700, 'Sam': 'manager', (3, 124): 7, 'pi': 3.14, 'v_1': [1, 0, 2]}
>>>
>>>
>>> d['Sam' : 'pi']
TypeError: unhashable type: 'slice'

Единственный способ извлечения значения элемента - это указание его ключа в квадратных скобках []

>>> d['price']
2700
>>>
>>> d['pi']
3.14
>>>
>>> d['v_1']
[1, 0, 2]

Зачем же тогда нажна такая неупорядоченная и странная структура данных? Дело в том что иногда вовсе не важно как упорядочены элементы, а важно лишь однозначно идентифицировать эти элементы. Для примера, представим аккаунты людей на Facebook социальные связи между которыми можно изобразить в виде графа:

Фрагмент социального графа для демонстрации работы с типом данных dict Python

Один из способов хранения данных о таких графах это матрицы:

Матричное представление социального графа для демонстрации работы с типом данных dict Python

Если на пересечении строки и столбца стоит \(1\) то это значит, что люди в социальной сети являются "друзьями", а если \(0\) - то люди никак не связаны между собой. С помощью списков данная матрица может быть представлена так:

>>> a = [[0, 0, 1, 0, 0],    # разреженная матрица в виде вложенных списков
[0, 0, 1, 1, 0],
[1, 1, 0, 0, 0],
[0, 1, 0, 0, 1], 
[0, 0, 0, 1, 0]]

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

>>> names = ['Sara', 'Sam', 'Anna', 'Tom', 'Mari']

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

>>> a[names.index('Sam')][names.index('Anna')]
1

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

>>> a = {('Sara', 'Anna'): 1,
         ('Sam', 'Anna'): 1,
         ('Sam', 'Tom'): 1,
         ('Tom', 'Mari'):1}

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

>>> a.get(('Sam', 'Mari'), 0)
0

Метод .get(('Sam', 'Mari'), 0) возвращает значение элемента с ключом ('Sam', 'Mari'), а если элемента с таким ключом нет, то возвращается указанное значение по умолчанию. Но у данного способа, есть еще один недостаток - важность следования имен в кортеже, т.е. несмотря на то что 'Sam' и 'Anna' являются "друзьями", мы можем не узнать об этом:

>>>> a.get(('Anna', 'Sam'), 0)
0
>>>
>>> a.get(('Sam', 'Anna'), 0)    # Хотя на самом деле они "друзья"
1

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

>>> a = {'Sara': ['Anna'],
         'Sam': ['Anna', 'Tom'],
         'Tom': ['Mari'],
         'Anna': ['Sara', 'Sam'],
         'Mari': ['Tom']}

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

>>> a['Anna']
['Sara', 'Sam']

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

>>> 'Sam' in a['Anna']
True
>>>
>>> 'Sam' in a['Mari']
False

Социальный граф - это лишь верхушка айсберга множества структур данных, в которых порядок следования элементов не важен или эта важность отодвигается на второй план. Примерами таких структур могут выступать хеш-таблицы, JSON ассоциативные массивы, кеши и т.д. Как это ни странно, но словари могут оказаться весьма удобны и для хранения таблиц, позволяя идентифицировать их строки и столбцы. Например, более подробную информацию о выдуманных выше персонажах можно хранить вот так так:

>>> data = {'Sara': {'gender': 'f', 'age': 21, 'country': 'USA'},
            'Sam': {'gender': 'm', 'age': 27, 'country': 'Canada'},
            'Tom': {'gender': 'm', 'age': 34, 'country': 'USA'},
            'Anna': {'gender': 'f', 'age': 23, 'country': 'Canada'},
            'Mari': {'gender': 'f', 'age': 19, 'country': 'USA'}}

На первый взгляд, выглядит несколько громоздко, но зато мы можем получать информацию из такого словаря, пользуясь вполне осмысленными запросами:

>>> data['Sam']['age']
27
>>>
>>> data['Mari']['country']
'USA'

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

>>> [i for i in data if data[i]['gender'] == 'f']
['Sara', 'Anna', 'Mari']

Или узнать имена людей проживающих в Канаде:

>>> [i for i in data if data[i]['country'] == 'Canada']
['Sam', 'Anna']

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


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

Литералом словарей служит пара фигурных скобок {}, так что пустой словарь может быть создан вот так:

>>> a = {}

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

>>> a = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}
>>> a
{'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}

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

>>> a = dict()
>>> a
{}

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

>>> a = dict(key1='value1', key2='value2', key3='value3')
>>> a
{'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}

Обратите внимание, что в перечислении именованных аргументов функции dict(key1='value1', key2='value2', key3='value3') будущие ключи элементов словаря 'key1', 'key2', 'key3' указываются без апострофов ''. Такой способ создания словаря очень хорош, если в качестве ключей могут использоваться только строки (а точнее допустимые идентификаторы имен переменных).

Еще один способ создания словарей заключается в том что бы передать функции dict() список с кортежами пар "ключ-значение":

>>> a = dict([('key1', 'value1'), ('key2', 'value2'), ('key3', 'value3')])
>>> a
{'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}

Кстати, функции dict() можно передать итератор, который возвращает функция zip()

>>> a = dict(zip(('key1', 'key2', 'key3'), ('value1', 'value2', 'value3')))
>>> a
{'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}

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

a = dict([('key' + str(i), 'value' + str(i)) for i in range(1, 4)])
>>> a    # словарь созданный из генератора списков
{'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}
>>>
>>>
>>> data = (('key' + str(i), 'value' + str(i)) for i in range(1, 4))
>>> data     # data - это выражение-генератор
<generator object <genexpr> at 0x0104C680>
>>>
>>> a = dict(data)    # из генераторов тоже может быть создан словарь
>>> a
{'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}

Ну и наконец, функции dict() может быть передан словарь, который возвращается как бы без изменений, но на самом деле возвращается его поверхностная копия:

>>> b = dict({'key1': 'value1', 'key2': 'value2', 'key3': 'value3'})
>>> b
{'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}

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

>>> a = {'key1': 'value1', 'key1': 'value2', 'key1': 'value3'}
>>> a
{'key1': 'value3'}

А вот значениями элементов могут быть абсолютно любые объекты языка Python, в том числе и одинаковые элементы:

>>> a = {'key1': 0, 'key2': 0, 'key3': 0}
>>> a
{'key1': 0, 'key2': 0, 'key3': 0}

Словари - это отображения

Единственное отличие словарей от последовательностей заключается в том что они не поддерживают оператор извлечения среза [START:STOP:STEP], но так же как и последовательности они поддерживают оператор вхождения in, функцию определения размера len(), а так же механизм итерирования (обхода в цикле) с помощью конструкции for... in....

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

>>> a = dict(zip(('abcdef'), (11, 22, 33, 44, 55, 66)))
>>> a
{'a': 11, 'b': 22, 'c': 33, 'd': 44, 'e': 55, 'f': 66}

А теперь попробуем извлечь из него элементы:

>>> a['b']
22
>>>
>>> a['e']
55

Изменить некоторый элемент в словаре можно с помощью оператора = следующим образом:

>>> a['f'] = -777
>>> a
{'a': 11, 'b': 22, 'c': 33, 'd': 44, 'e': 55, 'f': -777}
>>>
>>> a['d'] = -555
>>> a
{'a': 11, 'b': 22, 'c': 33, 'd': -555, 'e': 55, 'f': -777}

Если в квадратных скобках указать несуществующий ключ, то это приведет к ошибке KeyError:

>>> a['z']
KeyError: 'z'

Однако, если присвоить несуществующему ключу новое значение, то в словаре появится новая пара "ключ-значение":

>>> a['z'] = -999    # добавит в словарь элемент 'z': -999
>>> a
{'a': 11, 'b': 22, 'c': 33, 'd': -555, 'e': 55, 'f': -777, 'z': -999}
>>>
>>> a['w'] = -888    # добавит в словарь элемент 'w': -888
>>> a
{'a': 11, 'b': 22, 'c': 33, 'd': -555, 'e': 55, 'f': -777, 'z': -999, 'w': -888}

Квадратные скобки могут использоваться для удаления элементов с помощью инструкции del:

>>> del a['a']
>>> a
{'b': 22, 'c': 33, 'd': -555, 'e': 55, 'f': -777, 'z': -999, 'w': -888}
>>>
>>> del a['c']
>>> a
{'b': 22, 'd': -555, 'e': 55, 'f': -777, 'z': -999, 'w': -888}

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

>>> 'z' in a
True
>>>
>>> 'x' in a
False

И наоборот, что бы убедиться в том, что некоторого элемента нет в словаре, нужно воспользоваться конструкцией not in:

>>> 'z' not in a
False
>>>
>>> 'x' not in a
True

Однако, оператор in (not in) позволяет убедиться только в том что словарь содержит (или не содержит) элемент с заданным ключом. Если нужно проверить наличие некоторого значения, а не ключа то можно воспользоваться методом .values() который возвращает объект со всеми значениями словаря:

>>> a.values()
dict_values([22, -555, 55, -777, -999, -888])
>>>
>>> 22 in a.values()
True
>>>
>>> -333 in a.values()
False

Узнать размер словаря можно с помощью функции len():

>>> len(a)
6

Но будте внимательны, так как len() не учитывает размер вложенных структур, таких как словари, списки и множества, а только количество объектов внутри словаря:

>>> len({'a': [1, 2, 3], 'b': [4, 5, 6]})
2

Словари - это коллекции

Словари считаются коллекциями, потому что значениями их элементов могут быть абсолютно любые объекты языка Python и храниться в одном словаре могут объекты абсолютно разных типов:

>>> A = dict(a=1, b=3.14, c='abcd', d=[1,2,3], e={'X': 1, 'Y': 2})
>>> A
{'a': 1, 'b': 3.14, 'c': 'abcd', 'd': [1, 2, 3], 'e': {'X': 1, 'Y': 2}}

Как видите, в словаре a содержатся объекты разных типов, причем обращаясь к ним по соответствующему ключу, с ними можно выполнять различные операции:

>>> A['a'] + 10
11
>>> A['b']**2
9.8596

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

>>> A['c']
'abcd'

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

>>> A['c'][3]     # или A['c'][-1]
'd'

То же самое касается и вложеных списков:

>>> A['d'][1]
2
>>>
>>> A['d'][:2]     # можно даже извлекать срезы
[1, 2]
>>> 
>>> A['d'][3:] = [4]     # или менять список
>>> A
{'a': 1, 'b': 3.14, 'c': 'abcd', 'd': [1, 2, 3, 4], 'e': {'X': 1, 'Y': 2}}

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

>>> A['e']['Y']
2
>>> A['e']['Y']**10
1024

Словари могут хранить функции и классы:

>>> F = {'a': int, 'b': str, 'c': sum}
>>> F
{'a': <class 'int'>, 'b': <class 'str'>, 'c': <built-in function sum>}

Теперь мы можем пользоваться используя следующие выражения:

>>> # переведем число из пятиричной системы счисления в десятичную:
>>> F['a']('1234', 5)
194
>>>
>>> # равносильно использованию функции int:
>>> int('1234', 5)
194
>>>
>>> F['b'](1234)
'1234'
>>> str(1234)
'1234'
>>>
>>> F['c']([1, 2, 3, 4])
10
>>> sum([1, 2, 3, 4])
10

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


Поверхностные и глубокие копии словарей

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

>>> a = dict(a=1, b=2, c=3)
>>> a
{'a': 1, 'b': 2, 'c': 3}
>>>
>>> b = a

Сейчас b и a ссылаются на один и тот же словарь, теперь изменим словарь посредством переменной a:

>>> a['c'] = 333
>>> a
{'a': 1, 'b': 2, 'c': 333}

Но и b теперь ссылается на тот же самый словарь:

>>> b
{'a': 1, 'b': 2, 'c': 333}

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

>>> # теперь изменения в 'a'
>>> a['b'] = 222
>>> a
{'a': 1, 'b': 222, 'c': 333}
>>>
>>> # никак не повлияют на 'b'
>>> b
{'a': 1, 'b': 2, 'c': 333}

Однако, поверхностное копирование не позволяет скопировать данные из вложенных списков и словарей:

>>> a = [1, 2, 3]
>>> b = {'a':1, 'b': 2}
>>>
>>> c = dict(k1=1, k2=2, k3=a, k4=b)
>>> c
{'k1': 1, 'k2': 2, 'k3': [1, 2, 3], 'k4': {'a': 1, 'b': 2}}

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

>>> d = c.copy()
>>> d
{'k1': 1, 'k2': 2, 'k3': [1, 2, 3], 'k4': {'a': 1, 'b': 2}}
>>>
>>> # Изменение поверхностных данных через 'c'
>>> c['k1'] = 111
>>> c
{'k1': 111, 'k2': 2, 'k3': [1, 2, 3], 'k4': {'a': 1, 'b': 2}}
>>>
>>> # никак не отразится на данных из 'd'
>>> d
{'k1': 1, 'k2': 2, 'k3': [1, 2, 3], 'k4': {'a': 1, 'b': 2}}

Но данные вложенного списка и словаря являются общими:

>>> c['k3'][0] = 111
>>> c['k4']['a'] = 111
>>> c
{'k1': 111, 'k2': 2, 'k3': [111, 2, 3], 'k4': {'a': 111, 'b': 2}}
>>>
>>> d
{'k1': 1, 'k2': 2, 'k3': [111, 2, 3], 'k4': {'a': 111, 'b': 2}}

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

>>> import copy

А затем, с помощью функции deepcopy() выполняем глубокое копирование:

>>> d = copy.deepcopy(c)

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

>>> c['k3'][1] = 222
>>> c['k4']['b'] = 222
>>> c
{'k1': 111, 'k2': 2, 'k3': [111, 222, 3], 'k4': {'a': 111, 'b': 222}}
>>>
>>> d
{'k1': 111, 'k2': 2, 'k3': [111, 2, 3], 'k4': {'a': 111, 'b': 2}}