Основы программирования в Python

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

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

Кортежи. Словари.

Кортежи (tuples)

Кортежи встречаются не только в программировании, но и в математике. В математике под кортежем обычно понимают упорядоченное множество. Про множества мы немного говорили на прошлой лекции, множество ‒ это просто набор каких-то элементов, причем элементы множества не должны повторятся. Порядок элементов в множестве не важен: {1, 2, 3} и {3, 2, 1} ‒ это одно и то же множество. В кортеже мы точно знаем, какой элемент является первым, какой ‒ вторым, и так далее.

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

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)

Словари (dictionaries)

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

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

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

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

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

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

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

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

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

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

KeyError: 'Esmeralda'

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

TypeError: 'dict_keys' object does not support indexing

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fiori plays the role of Phoebus

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

In [36]:
my_dict = {} # пустой словарь
In [37]:
my_dict[1] = 1
my_dict['hello'] = 'world'
In [38]:
my_dict # уже не пустой
Out[38]:
{1: 1, 'hello': 'world'}

Чтобы понять, каким образом еще можно получить словарь, полезно вспомнить, какие объекты лежат в dict_items().

In [39]:
my_dict.items()
Out[39]:
dict_items([(1, 1), ('hello', 'world')])

Как мы уже обсуждали, dict_items() ‒ это список кортежей. Значит, из списка кортежей можно, в свою очередь, получить словарь!

In [40]:
my_new_dict = dict([(1, 2), ("Hello", "World")])
In [41]:
my_new_dict
Out[41]:
{1: 2, 'Hello': 'World'}

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

In [42]:
students = ["Веня", "Сеня", "Каролина", "Сабрина"]
grades = [6, 7, 8, 9]

Воспользуемся функцией zip().

In [43]:
list(zip(students, grades))
Out[43]:
[('Веня', 6), ('Сеня', 7), ('Каролина', 8), ('Сабрина', 9)]

Как следует из названия, функция zip() действует как "молния": соединяет две части, то есть два списка, делая из пары списков список пар. Удобно то, что эта функция может соединять не только два списка, но и больше.

In [44]:
rating = [3, 2, 4, 1] # место в рейтинге
In [45]:
list(zip(students, grades, rating))
Out[45]:
[('Веня', 6, 3), ('Сеня', 7, 2), ('Каролина', 8, 4), ('Сабрина', 9, 1)]

Кроме того, функция zip() полезна тем, что помогает создавать словари:

In [46]:
gradebook = dict(list(zip(students, grades)))
gradebook
Out[46]:
{'Веня': 6, 'Каролина': 8, 'Сабрина': 9, 'Сеня': 7}

Еще о списках: функция enumerate()

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

In [47]:
students
Out[47]:
['Веня', 'Сеня', 'Каролина', 'Сабрина']

Как этого добиться, используя уже знакомые способы? Можно подключить к решению этой задачи функцию range()и задать интервал от 0 до числа элементов в списке (как мы помним, правый конец исключается):

In [48]:
for i in range(0, len(students)):
    print(i, students[i])
0 Веня
1 Сеня
2 Каролина
3 Сабрина

Но, как всегда, есть готовый и более удобный способ ‒ функция enumerate(). Эта функция создает пары индекс элемента-значение элемента:

In [49]:
list(enumerate(students))
Out[49]:
[(0, 'Веня'), (1, 'Сеня'), (2, 'Каролина'), (3, 'Сабрина')]

Поэтому работать в ней можно так же, как с методом .items() у словарей. Например, так:

In [50]:
for i, v in enumerate(students):
    print(i, v)
0 Веня
1 Сеня
2 Каролина
3 Сабрина

Здесь i ‒ индекс элемента в списке, v ‒ его значение. И опять же, неважно, как называть эти переменные: Python знает, что если после перечисления переменных в for, то первая переменная отвечает за индекс, а вторая ‒ за значение.