Текст лекции: Щуров И.В., НИУ ВШЭ
Данный notebook является конспектом лекции по курсу «Программирование на языке Python для сбора и анализа данных» (НИУ ВШЭ, 2015-16). Он распространяется на условиях лицензии Creative Commons Attribution-Share Alike 4.0. При использовании обязательно упоминание автора курса и аффилиации. При наличии технической возможности необходимо также указать активную гиперссылку на страницу курса. Фрагменты кода, включенные в этот notebook, публикуются как общественное достояние.
Другие материалы курса, включая конспекты и видеозаписи лекций, а также наборы задач, можно найти на странице курса.
Рассмотрим такую задачу: у нас есть информация об оценках студентов по некоторому предмету и мы хотим иметь возможность с этой информацией работать — например, по имени студента определить, какую оценку он получил. Мы могли бы пытаться решить эту задачу, создав два списка — один с именами студентов, а другой с оценками:
students = ["Вася", "Коля", "Петя", "Аня"]
grades = [5, 4, 2, 3]
# Вася получил 5, Коля 4 и т.д.
В принципе, мы могли бы теперь, зная имя студента, найти его номер в первом списке и потом обратиться к элементу второго списка с тем же номером. Однако, этот подход довольно неэффективен: если бы у нас была тысяча студентов, нам пришлось бы сначала долго-долго просматривать их список в поисках нужного имени. Время, необходимое на поиск нужной информации, росло бы вместе с ростом длины списка. Это плохо.
Было бы здорово, если бы у нас была возможность иметь тип данных, в котором элементы нумеруются не натуральными числами, а произвольными объектами. Оказывается, такой тип данных существует: в Python он называется словарём (dictionary).
Более общий термин для такого типа данных: ассоциированный массив; в других языках программирования используются также другие термины — например, в Perl похожий объект называется hash — сокращение от hash table.
Вот так можно создать словарь в Python:
gradebook = {"Вася": 5, "Коля": 4, "Петя":2, "Аня": 3}
Это похоже на создание списка, но есть ряд отличий. Во-первых, мы использовали фигурные скобки вместо квадратных, чтобы показать, что создаём именно словарь. Во-вторых, словарь состоит из записей, каждая запись состоит из двух частей: ключа (key) и значения (value). Ключ и значение разделяются двоеточием. Например, у нас есть запись "Аня": 3
с ключом "Аня"
и значением 3
. Всего наш словарь gradebook
сейчас содержит четыре записи, ключами которых являются имена студентов, а значениями — их оценки.
gradebook
{'Аня': 3, 'Вася': 5, 'Коля': 4, 'Петя': 2}
Заметим, что при печати Python переупорядочил записи в словаре. На самом деле, порядок вывода записей в словаре является произвольным: внутри словаря записи не имеют никакого порядка. Поэтому нельзя обратиться, например, к «первой записи», но зато можно обратиться к записи с данным ключом:
gradebook['Аня']
3
gradebook['Вася']
5
Можно изменить значение записи, точно так же, как изменить элемент списка.
gradebook['Аня'] = 5 # Аня переписала контрольную!
gradebook
{'Аня': 5, 'Вася': 5, 'Коля': 4, 'Петя': 2}
Можно добавить новую запись.
gradebook['Иннокентий'] = 4 # О, новенький!
gradebook
{'Аня': 5, 'Вася': 5, 'Иннокентий': 4, 'Коля': 4, 'Петя': 2}
При попытке обратиться к записи, которой нет, мы получим сообщение об ошибке:
gradebook['Alice']
--------------------------------------------------------------------------- KeyError Traceback (most recent call last) <ipython-input-10-ffb66a08ba85> in <module>() ----> 1 gradebook['Alice'] KeyError: 'Alice'
Часто нам хочется иметь возможность запросить запись, а в случае, если её нет, получить какое-нибудь «значение по умолчанию», а не ошибку. Для этого нужно использовать метод get()
вместо квадратных скобок.
gradebook.get('Alice')
Здесь вернулось None
:
print(gradebook.get('Alice'))
None
gradebook.get('Вася')
5
Можно было бы передать get()
второй аргумент, и тогда в случае, если такого ключа в словаре нет, то будет возвращен он.
gradebook.get('Alice', 'No such student')
'No such student'
gradebook.get('Вася', 'No such student')
5
Можно получить список всех ключей словаря:
gradebook.keys()
dict_keys(['Петя', 'Вася', 'Коля', 'Аня', 'Иннокентий'])
На самом деле это не совсем список, но эта штука ведёт себя почти как список и из неё можно сделать список. Аналогично со списком всех значений словаря.
gradebook.values()
dict_values([2, 5, 4, 5, 4])
Ключами словарей могут быть не только строчки. Допустим, мы хотим создать словарь, в котором ключами будут числа. Нет ничего проще:
squares={1:1, 2:4, 3:9}
squares
{1: 1, 2: 4, 3: 9}
squares[1]
1
squares[2]
4
В предыдущих двух строчках squares
ведёт себя примерно как список, но если внимательно приглядеться, то видно, что это не список, а всё-таки словарь.
squares
{1: 1, 2: 4, 3: 9}
Например, у любого непустого списка есть элемент с индексом 0, а у squares
такого нет:
squares[0]
--------------------------------------------------------------------------- KeyError Traceback (most recent call last) <ipython-input-23-58a7a2576c1f> in <module>() ----> 1 squares[0] KeyError: 0
Как обрабатывать информацию в словаре? Для перебора всех элементов списка можно было использовать цикл for
. А что будет, если ему скормить словарь вместо списка? Попробуем:
for k in gradebook:
print(k)
Петя Вася Коля Аня Иннокентий
Понятно! Цикл for
в этом случае перебирает все ключи нашего словаря. А зная ключ, можно получить и значение:
for k in gradebook:
print("Студент", k, "имеет оценку", gradebook[k])
Студент Петя имеет оценку 2 Студент Вася имеет оценку 5 Студент Коля имеет оценку 4 Студент Аня имеет оценку 5 Студент Иннокентий имеет оценку 4
Однако, есть более изящный способ получить сразу ключ и значение очередной записи: использовать items()
.
for k, v in gradebook.items():
print("Студент",k,"имеет оценку", v)
Студент Петя имеет оценку 2 Студент Вася имеет оценку 5 Студент Коля имеет оценку 4 Студент Аня имеет оценку 5 Студент Иннокентий имеет оценку 4
Как работает этот код? Здесь используется метод items()
, возвращающий список (точнее, итератор), состоящий из кортежей вида (ключ, значение)
.
list(gradebook.items())
[('Петя', 2), ('Вася', 5), ('Коля', 4), ('Аня', 5), ('Иннокентий', 4)]
Оператор for
в этом случае понимает, что нужно при каждом проходе цикла выбрать очередной кортеж и присвоить его первый элемент (то есть ключ) переменной k
, а второй элемент (то есть значение) переменной v
(конечно, эти переменные могли бы называться иначе). С аналогичным поведением мы уже встречались, когда обсуждали конструкцию enumerate
(см. лекцию №3).
Вот так можно найти все записи с заданным значением — например, всех студентов, получивших оценку 4:
for k, v in gradebook.items():
if v==4:
print(k)
Коля Иннокентий
Заметим, что такой «поиск по значению» требует перебора всех записей в словаре и если словарь большой, то он будет занимать много времени — хотя «поиск по ключу» будет по-прежнему выполняться быстро. Кстати, можно быстро проверить, существует ли в словаре запись с данным ключом:
"Коля" in gradebook
True
"Alice" in gradebook
False
Если бы мы хотели искать среди значений, то нужно было бы явно это указать с помощью метода values()
:
1 in gradebook.values()
False
4 in gradebook.values()
True
Вообще оператор in
не ограничивается только использованием со словарями: он может использоваться, например, со списками:
5 in [1,2,3,5,8]
True
6 in range(1,5)
False
zip()
¶Есть разные способы создавать словари. Например, можно создать пустой словарь и постепенно заполнять его элементами:
my_dict = {}
my_dict[1] = 1
my_dict['hello'] = 'world'
my_dict
{1: 1, 'hello': 'world'}
Заметим, что в одном и том же словаре прекрасно уживаются элементы разных типов (в данном случае — строки и целые числа).
Можно создать словарь иначе, передав функции dict()
список, состоящий из пар ключ-значение (в некоторо смысл, это обратная операция методу items()
):
dict([('hello','world'), ('one', 'two')])
{'hello': 'world', 'one': 'two'}
Вернёмся к началу лекции. Допустим, у нас есть два списка, в одном находятся имена студентов, а в другом их оценки. Как можно из этих списков создать словарь, для которого имена были бы ключами, а оценки значениями?
А вот так:
students = ["Вася", "Коля", "Петя", "Аня"]
grades = [5, 4, 2, 3]
new_gradebook = list(zip(students,grades))
new_gradebook
[('Вася', 5), ('Коля', 4), ('Петя', 2), ('Аня', 3)]
Здесь используется удобная функция zip()
, применение которой не ограничивается созданием словарей. Подобно застёжки-молнии, она «состёгивает» (отсюда и название) несколько списков. Например, zip()
делает из пары списков список пар (утверждение звучит как скороговорка, но если вы подумаете нам ним как следует, то заметите, что оно в точности описывает то, что делает эта команда):
list(zip([1,2,3],['a','b','c']))
[(1, 'a'), (2, 'b'), (3, 'c')]
Эту конструкцию можно использовать, когда нам нужно перебрать элементы двух связанных между собой списков. Например, вот так можно вывести информацию о том, какой студент какую оценку имеет, не используя словари:
for student, grade in zip(students, grades):
print(student, "has grade", grade)
Вася has grade 5 Коля has grade 4 Петя has grade 2 Аня has grade 3
Функцию zip()
можно использовать и более чем с двумя списками:
list(zip([1,2,3,4], [5,6,7,8], ['a','b','c','d']))
# списка три, поэтому на выходе получится список из троек
[(1, 5, 'a'), (2, 6, 'b'), (3, 7, 'c'), (4, 8, 'd')]
Если какой-то из списков окажется короче, то zip()
«обрежет» остальные списки:
list(zip([1,2,3], ['a','b']))
[(1, 'a'), (2, 'b')]
До сих пор мы рассматривали словари, ключами которых являются строки и числа. На самом деле, ключами могут быть и более сложно устроенные объекты. Например, представим себе такую реализацию фрагмента таблицы сложения в виде словаря:
sums = {(2,3): 5, (4, 1): 5, (5, 7): 12}
sums
{(2, 3): 5, (4, 1): 5, (5, 7): 12}
Здесь ключами являются кортежи, состоящие из двух чисел, а значениями — суммы этих чисел.
sums[(2,3)]
5
sums[(4,1)]
5
sums[(5,7)]
12
В этом месте проявляется важное отличие кортежей от списков: последние не могут быть ключами словарей, поскольку могут изменяться.
sums = { [1,2]: 3}
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-49-16e6151262ef> in <module>() ----> 1 sums = { [1,2]: 3} TypeError: unhashable type: 'list'
На этом мы закончим краткое введение в словари и перейдём к следующей теме.
Мы ранее частенько сталкивались с такой задачей: дан список, в котором записаны числа, но в виде строчек. Создать новый список, в котором числа были бы числами. Мы могли решить эту задачу с помощью цикла:
str_list = ["1", "5", "12", "7"]
int_list = []
for s in str_list:
int_list.append(int(s))
print(int_list)
[1, 5, 12, 7]
За создание нового списка отвечают три строчки. Писать их каждый раз довольно тоскливо, и создатели Python придумали (точнее, заимствовали из функциональных языков программирования, а те его заимствовали у математиков) гораздо более изящный синтаксис. Он устроен вот так:
int_list = [int(s) for s in str_list]
Кадратные скобки вокруг выражения должна подсказать, что мы создаём список (потому что когда нужно создать список, мы обычно заключаем его элементы в квадратные скобки). Выражение внутри скобок нужно читать буквально:
список, состоящий из элементов int(s)
для (for
) элементов s
из (in
) списка str_list
Представьте себя Гарри Поттером, у которого есть волшебная палочка, превращающая строки в числа. Мы подействовали этой волшебной палочкой на все элементы списка str_list
.
int_list
[1, 5, 12, 7]
Видите? Кавычки исчезли — перед нами список, состоящий из чисел. Магия! Исходный список str_list
при этом не изменился:
str_list
['1', '5', '12', '7']
Аналогично можно применять любую операцию к элементам списка. Например, возведём все элементы из int_list
в квадрат (Гарри Поттер взял другую волшебную палочку):
[x**2 for x in int_list]
[1, 25, 144, 49]
или удвоим все элементы списка:
double_list = [x*2 for x in int_list]
double_list
[2, 10, 24, 14]
или прибавим к ним 1:
[x+1 for x in int_list]
[2, 6, 13, 8]
или превратим их в числа с плавающей точкой:
[float(x) for x in int_list]
[1.0, 5.0, 12.0, 7.0]
Как видите, с элементами списков можно делать что угодно! Однако, это ещё не все. В синтаксисе списочных включений можно производить фильтрацию. Например, нам нужны только те элементы, которые больше 6. Мы можем их выбрать таким образом:
[x for x in int_list if x > 6]
[12, 7]
Когда мы пишем здесь x for x
мы имеем в виду, что нужно просто подставить в новый список элементы старого, ничего с ними не делая (только выбирая нужные). Но можно и как-то их модифицировать:
[x**2 for x in int_list if x > 6]
[144, 49]
Решим теперь такую задачу: есть два списка с числами, а мы хотим найти их поэлементную сумму.
X = [2, 5, 8]
Y = [1, 3, 100]
Её можно решить таким образом (для перебора элементов двух списков одновременно используем конструкцию zip()
, обсуждавшуюся выше):
Z = []
for x, y in zip(X, Y):
Z.append(x + y)
print(Z)
[3, 8, 108]
Но со списочными включениями тот же код выглядит гораздо симпатичнее:
[x + y for x, y in zip(X, Y)]
[3, 8, 108]
Кстати, можно использовать синтаксис, похожий на списочное включение, чтобы создавать словари:
squared = {i: i**2 for i in range(10)} # обратите внимание на фигурные скобки!
squared
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
map()
¶У списочных включений есть аналог, который сейчас считается не слишком удобным, но иногда встречается: функция map()
. Вернёмся к задаче про прервращение строчек в числа.
str_list
['1', '5', '12', '7']
Вот так она решается с помощью map()
:
int_list = list(map(int, str_list))
list(int_list)
[1, 5, 12, 7]
Функция map()
принимает два аргумента. Первым аргументом она принимает функцию (да-да, в Python функции можно передавать другим функциям в качестве аргументов!), а вторым — список. После этого она применяет эту функцию к каждому из элементов списка. В общем, записи вида list(map(int, str_list))
и [int(x) for x in str_list]
почти эквивалентны.
Когда действие, которое нужно применить, уже существует в виде функции (как в случае с int
), то конструкция с map()
выглядит даже более лаконичной, чем списочное включение. Но если нам нужно сделать что-то менее тривиальное, списочные включения явно проще:
[int(x)+1 for x in str_list]
[2, 6, 13, 8]
Чтобы реализовать это с помощью map()
, нужно объявить новую функцию, которая будет возвращать значение выражения int(x)+1
и передать её map()
.
def my_func(x):
return int(x)+1
list(map(my_func,str_list))
[2, 6, 13, 8]
Для краткости можно использовать lambda
-функции (мы про них как-нибудь ещё поговорим), но такой подход гораздо менее прозрачен, чем списочные включения, и сейчас использовать его не рекомендуется.
Использовать списочные включения не только приятно, но и полезно: они работают эффективнее, чем код с циклом.
from random import random
from math import sqrt
N = 10000
mylist = [random() for _ in range(N)]
%%timeit
newlist = []
for x in mylist:
newlist.append(sqrt(x))
1000 loops, best of 3: 1.5 ms per loop
%%timeit
newlist = [sqrt(x) for x in mylist]
1000 loops, best of 3: 787 µs per loop
%%timeit
newlist = list(map(sqrt, mylist))
1000 loops, best of 3: 792 µs per loop
Как видно из этих данных (магическое слово %timeit
позволяет измерить, сколько времени уходит на какую-то операцию), списочные включения почти в два раза быстрее обычного цикла. map()
работает примерно с такой же скоростью, как и списочные включения (иногда чуть медленнее, иногда чуть быстрее).
Списки позволяет сохранить некоторый ряд значений, но зачастую нужно уметь работать с более сложные структурами — например, с таблицами. В некоторых языках программирования есть двумерные массивы. Аналогом двумерного массива в Python является «список списков», то есть такой список, элементами которого являются другие списки. С чем-то подобным мы уже встречались.
Рассмотрим пример: таблица, в которой записаны результаты по нескольким домашним работам у нескольких студентов. (Допустим, мы присвоили студентам некоторые номера и поэтому нам не нужно знать, кого как зовут.) Её можно записать в виде списка списков, например, по строчкам:
table = [["HW1", "HW2", "HW3", "HW4"], [4, 3, 4, 4], [3, 4, 3, 4], [4, 5, 5, 4]]
Здесь каждый элемент списка table
— это строчка нашей таблицы, то есть тоже список. Например, вот так можно узнать, что записано в третьей строке и четвертом столбце нашей таблицы:
table[2][3]
4
Что здесь произошло? Мы сначала вызвали третью строку таблицы с помощью
table[2]
[3, 4, 3, 4]
А потом из этой третьей строки выбрали четвертый элемент с помощью [3]
. Можно было бы записать это более подробно:
row = table[2]
print(row[3])
# row[3] это то же самое, что table[2][3]
4
Вот так можно напечатать все элементы таблицы по строчкам:
for row in table:
print(*row)
HW1 HW2 HW3 HW4 4 3 4 4 3 4 3 4 4 5 5 4
Допустим теперь, что нам всё же хочется знать, какой студент какую оценку получил. Тогда мы могли бы вместо списка списков использовать словарь, у которого списки были бы значениями:
gradebook = {'Bill': [4, 3, 2], 'Alice': [3, 4, 5], 'Bob': [5, 5, 4]}
Вот так можно посмотреть, какую оценку получил Боб по второй домашке:
gradebook['Bob'][1]
5
На сегодня всё! :)