Программирование и анализ данных

Алла Тамбовцева, НИУ ВШЭ

Данный ноутбук частично основан на лекциях Щурова И.В., курс «Программирование на языке Python для сбора и анализа данных» (НИУ ВШЭ).

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

Кортежи (tuples)

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

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

In [1]:
my_tuple = (1, 2, 4, 6, 9)

К элементам кортежа можно обращаться точно так же, как к элементам списка:

In [2]:
my_tuple[0]
Out[2]:
1

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

In [3]:
my_tuple[2] = 65
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-a0083389ad43> in <module>()
----> 1 my_tuple[2] = 65

TypeError: 'tuple' object does not support item assignment

Иногда это свойство бывает полезным (некоторая «защита» от изменений), иногда – не очень, но для нас пока важно познакомиться с разными объектами в Python, чтобы потом не удивляться. Ведь многие более продвинутые функции могут возвращать результат или, наоборот, принимать на вход только кортежи или только списки.

При желании кортеж можно превратить в список:

In [4]:
list(my_tuple)
Out[4]:
[1, 2, 4, 6, 9]

И наоборот:

In [5]:
tuple([1,2,3])
Out[5]:
(1, 2, 3)

Если посмотреть на методы, применяемые к кортежам (например, набрать my_tuple. и нажать Tab), то можно заметить, что методов для кортежей сильно меньше по сравнению с методами для списков. Во многом это связано с тем, что кортеж нельзя изменить. Но вот «склеивать» кортежи, создавая при этом новый, легко:

In [6]:
(1, 3) + (7, 8)
Out[6]:
(1, 3, 7, 8)

Словари (dictionaries)

Обсуждая словари в Python, удобно проводить аналогию с обычными словарями (бумажными или электронными). Что такое словарь? Перечень из пар: слово-значение или слово-список значений, если значений несколько. Вот и словарь в Python – это объект, структура данных, которая позволяет хранить пары соответствий.

Давайте представим, что нам нужно создать словарь, который мы будем использовать для программки к мюзиклу "Notre Dame de Paris". Будем записывать в словарь prog пары соответствий герой-актер.

In [7]:
prog = {'Gringoire' : 'Pelletier', 
        'Frollo' : 'Lavoie', 'Phoebus': 'Fiori'}

Первый элемент в каждой паре (до двоеточия) назвается ключом (key), второй элемент в каждой паре (после двоеточия) – значением (value). Посмотрим на словарь:

In [8]:
prog
Out[8]:
{'Gringoire': 'Pelletier', 'Frollo': 'Lavoie', 'Phoebus': 'Fiori'}

Обращение к элементам словаря

Как и в случае со списками или кортежами, к элементам словаря можно обращаться. Только выбор элемента производится не по индексу, а по ключу: сначала указываем название словаря, а потом в квадратных скобках – ключ, по которому мы хотим вернуть значение. Например, выясним, кто играет роль Феба:

In [9]:
prog['Phoebus']
Out[9]:
'Fiori'

А что будет, если мы запросим элемент по ключу, которого нет в словаре?

In [10]:
prog['Esmeralda']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-10-a18d7b346a4e> in <module>()
----> 1 prog['Esmeralda']

KeyError: 'Esmeralda'

В глубине души Python начинает грустно петь "Where is she, your Esmeralda?", но вместо эмоций выдает сухое KeyError. Ошибка ключа – ну нет в словаре элемента с ключом Esmeralda!

Теперь представьте себе такую ситуацию: у нас есть список героев (ключей) и мы хотим в цикле вернуть по ним в фамилии актеров (значения). Какого-то одного из героев нет. Что произойдет? На каком-то этапе Python выдаст ошибку, мы вывалимся из цикла, и на этом наша работа остановится. Обидно, да? Чтобы такого избежать, получать значение по ключу можно другим способом – используя метод .get():

In [11]:
prog.get('Esmeralda') # ни результата, ни ошибки

Если выведем результат на экран явно, с помощью print(), увидим, что в случае, если пары с указанным ключом в словаре нет, Python выдаст значение None:

In [12]:
print(prog.get('Esmeralda'))
None

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

In [13]:
prog.get('Esmeralda', 'Not found')
Out[13]:
'Not found'

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

In [14]:
print(prog.get('Esmeralda', 99))
print(prog.get('Esmeralda', False))
99
False

Но недостающий элемент мы всегда можем добавить!

In [15]:
prog['Esmeralda'] = 'Segara'
prog
Out[15]:
{'Gringoire': 'Pelletier',
 'Frollo': 'Lavoie',
 'Phoebus': 'Fiori',
 'Esmeralda': 'Segara'}

Внимание: Если элемент с указанным ключом уже существует, новый с таким же ключом не добавится! Ключ – это уникальный идентификатор элемента. Если мы добавим в словарь новый элемент с уже существующим ключом, мы просто изменим старый – словари являются изменяемыми объектами. Например, так (изменения в программке prog):

In [16]:
prog['Esmeralda'] = 'Noa'
prog
Out[16]:
{'Gringoire': 'Pelletier',
 'Frollo': 'Lavoie',
 'Phoebus': 'Fiori',
 'Esmeralda': 'Noa'}

Раз элементами словаря являются пары ключ-значение, наверняка есть способ выбрать из словаря ключи и значения отдельно. Действительно, для этого есть методы .keys() и values(). Вызовем сначала все ключи:

In [17]:
prog.keys()
Out[17]:
dict_keys(['Gringoire', 'Frollo', 'Phoebus', 'Esmeralda'])

Объект, который мы только что увидели, очень похож на список. Но обычным списком на самом деле не является. Давайте попробуем выбрать первый элемент prog.keys():

In [18]:
keys = prog.keys()
keys[0]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-18-5481656211fb> in <module>()
      1 keys = prog.keys()
----> 2 keys[0]

TypeError: 'dict_keys' object does not support indexing

Не получается! Потому что полученный объект имеет специальный тип dict_keys, а не list. Но это всегда можно поправить, превратив объект dict_keys в список:

In [19]:
list(keys)[0] # получается!
Out[19]:
'Gringoire'

Аналогичным образом можно работать и со значениями:

In [20]:
prog.values()
Out[20]:
dict_values(['Pelletier', 'Lavoie', 'Fiori', 'Noa'])

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

In [21]:
numbers = {1 : 7, 2 : 8, 3 : 9}

Обращаться к элементам мы будем, естественно, без кавычек, так как ключами являются числа:

In [22]:
numbers[1] # оценка студента с id равным 1
Out[22]:
7

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

In [23]:
marks = {"Петя": 6, "Вася": 9}
In [24]:
marks["Петя"]
Out[24]:
6

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

In [25]:
my_dict = {'swear' : ['клясться', 'ругаться'], 
           'dream' : ['спать', 'мечтать']}

По ключу мы получим значение в виде списка:

In [26]:
my_dict['swear']
Out[26]:
['клясться', 'ругаться']

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

In [27]:
my_dict['swear'][0] # первый элемент
Out[27]:
'клясться'

Можем пойти дальше и создать словарь, где значениями являются словари! Например, представим, что в некотором сообществе проходят выборы, и каждый участник может проголосовать за любое число кандидатов. Данные сохраняются в виде словаря, где ключами являются имена пользователей, а значениями – пары кандидат-голос.

In [28]:
# '+' - за, '-' - против, 0 - воздержался
votes = {'user1': {'cand1': '+', 'cand2': '-'},
         'user2' : {'cand1': 0, 'cand3' : '+'}} 
In [29]:
votes
Out[29]:
{'user1': {'cand1': '+', 'cand2': '-'}, 'user2': {'cand1': 0, 'cand3': '+'}}

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

In [30]:
# берем значение, соответствующее ключу user1, в нем – ключу cand1
votes['user1']['cand1'] 
Out[30]:
'+'

А теперь давайте подумаем, как можно вывести на экран элементы словаря по очереди, в цикле. Первая попытка:

In [31]:
for k in prog:
    print(k)
Gringoire
Frollo
Phoebus
Esmeralda

Попытка не совсем удалась: на экран были выведены только ключи. А как вывести пары?

Задача: для каждого героя мюзикла из словаря prog вывести на экран сообщение вида

Fiori plays the role of Phoebus

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

In [32]:
for k  in prog:
    print(prog[k], "plays the role of", k)
Pelletier plays the role of Gringoire
Lavoie plays the role of Frollo
Fiori plays the role of Phoebus
Noa plays the role of Esmeralda

Упражнение полезное, но, на самом деле, существует специальный метод .items(), который позволяет обращаться сразу к парам элементов:

In [33]:
for k, v in prog.items():
    print(k, v)
Gringoire Pelletier
Frollo Lavoie
Phoebus Fiori
Esmeralda Noa

Для того, чтобы вывести и ключ, и значение, нужно в цикле for перечислить две переменные через запятую. И совсем не обязательно называть их k и v или key и value; Python сам поймет, что первая переменная соответствует ключу, а вторая ‒ значению. Для примера решим поставленную выше задачу с помощью items().

In [34]:
for hero, actor in prog.items():
    print(actor, "plays the role of", hero)
Pelletier plays the role of Gringoire
Lavoie plays the role of Frollo
Fiori plays the role of Phoebus
Noa plays the role of Esmeralda

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

In [35]:
prog.items()
Out[35]:
dict_items([('Gringoire', 'Pelletier'), ('Frollo', 'Lavoie'), ('Phoebus', 'Fiori'), ('Esmeralda', 'Noa')])

Как и случае с методами .keys() и .values(), полученный объект не является «обычным» списком. Поэтому при необходимости его нужно будет превратить в список:

In [36]:
list(prog.items())
Out[36]:
[('Gringoire', 'Pelletier'),
 ('Frollo', 'Lavoie'),
 ('Phoebus', 'Fiori'),
 ('Esmeralda', 'Noa')]

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

In [37]:
grades = {"Вася": 7, "Петя" : 9, "Коля" : 8, "Лена" : 8, 
          "Василиса" : 10}

И выведем на экран имена тех студентов, у которых оценка равна 8:

In [38]:
for name, grade in grades.items():
    if grade == 8:
        print(name)
Коля
Лена

Только два человека: Коля и Лена. А как проверить, есть ли в словаре элемент с определенным ключом? Воспользоваться оператором in:

In [39]:
"Коля" in grades.keys()
Out[39]:
True
In [40]:
"Ваня" in grades.keys()
Out[40]:
False