Python для сбора данных

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

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

Списки и цикл for

Знакомство со списками

Создадим список значений возраста респондентов, список age:

In [1]:
age = [23, 25, 32, 48, 19] # возраст
age
Out[1]:
[23, 25, 32, 48, 19]

Элементы списка перечисляются в квадратных скобках через запятую.

Можем создать список имен name, полностью состоящий из строк:

In [2]:
name = ["Анна", "Виктор", "Дмитрий", "Алёна", "Павел"] # имена

А можем создать смешанный список – список, состоящий из элементов разных типов. Представим, что не очень сознательный исследователь закодировал пропущенные значения в списке текстом, написал «нет ответа»:

In [3]:
mix = [23, 25, "нет ответа", 32, "нет ответа"] # все вместе

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

In [4]:
type(mix)
Out[4]:
list

У списка всегда есть длина – количество элементов в нём. Длина определяется с помощью функции len().

In [5]:
len(age) # пять элементов
Out[5]:
5

Если список пустой, то, как несложно догадаться, его длина равна нулю:

In [6]:
empty = []
len(empty)
Out[6]:
0

Пустой список можно создать двумя способами. Первый способ был продемонстрирован выше (квадратные скобки и ничего внутри), второй способ – использовать функцию list().

In [7]:
empty2 = list()
empty2
Out[7]:
[]

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

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

In [8]:
age[0] # первый элемент age
Out[8]:
23

Порядковый номер элемента в списке называется индексом. Далее, чтобы не путаться, будем разделять термины: порядковые числительные останутся для обозначения номера элемента в нашем обычном понимании, а индексы – для обозначения номера элемента в Python. Например, если нас будет интересовать элемент 25 из списка age, мы можем сказать, что нас интересует второй элемент или элемент с индексом 1:

In [9]:
print(age)
print(age[1])
[23, 25, 32, 48, 19]
25

Если элемента с интересующим нас индексом в списке нет, Python выдаст ошибку, а точнее, исключение, IndexError.

In [10]:
age[7]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-10-979e3832d406> in <module>()
----> 1 age[7]

IndexError: list index out of range

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

In [11]:
age[len(age)-1] # последний элемент - 19
Out[11]:
19

Конечно, в том, что нумерация элементов в списке начинается с нуля, есть некоторое неудобство – индекс последнего элемента не совпадает с длиной списка. Но, на самом деле, обращаться к последнему элементу списка можно и по-другому: считать элементы с конца!

In [12]:
age[-1] # последний элемент - он же первый с конца
Out[12]:
19

Отрицательные индексы элементов в Python – абсолютно нормальная вещь. Можем так же получить второй элемент с конца:

In [13]:
age[-2]
Out[13]:
48

Изменение и добавление элементов

Список – изменяемый объект в Python (mutable type). Элементы списка можно изменять, внося изменения прямо в нужный список, то есть не создавая при этом новую переменную.

In [14]:
age[0] = 32 # заменили первый элемент на 32
age
Out[14]:
[32, 25, 32, 48, 19]

А еще можно дописывать элементы в конец списка. Для этого существует два метода: .append() и .extend(). Метод .append() используется для присоединения одного элемента, .extend() – для добавления целого списка. Для примера создадим список nums:

In [15]:
nums = [1, 5, 8, 9]
In [16]:
nums.append(10) # добавили 10
nums
Out[16]:
[1, 5, 8, 9, 10]
In [17]:
nums.extend([12, 13]) # добавили 12 и 13
nums
Out[17]:
[1, 5, 8, 9, 10, 12, 13]

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

In [18]:
L = []
L.append(6)
L.append(8)
L
Out[18]:
[6, 8]

Методы .append() и .extend() приписывают значения только в конец списка. Для добавления элементов в любое другое место существует метод .insert(), и мы поговорим о нем чуть позже.

Важно: если поменять местами .append() и .extend(), код либо не будет работать (случай 1), либо будет работать не так, как хочется (случай 2).

In [19]:
nums.extend(6) # случай 1: один элемент не добавится
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-19-245a5c1e6216> in <module>()
----> 1 nums.extend(6) # случай 1: один элемент не добавится

TypeError: 'int' object is not iterable
In [20]:
nums.append([2, 4]) # случай 2: добавится целый список, прямо в квадратных скобках
nums
Out[20]:
[1, 5, 8, 9, 10, 12, 13, [2, 4]]

Сразу отметим важную деталь: при работе со списками не нужно лишний раз ставить квадратные скобки. Да, они используются для создания списков, но если объект уже является списком, еще одни скобки будут неуместны. Другими словами, объекты age и [age] – совершенно разные!

In [21]:
print(age)
print([age])
[32, 25, 32, 48, 19]
[[32, 25, 32, 48, 19]]

Здесь [age] – это список списков. Такой объект тоже иногда бывает полезен, но просто так создавать его не нужно. Из объекта [age] выбрать элемент с индексом 2 уже не получится:

In [22]:
[age][2]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-22-130162b30080> in <module>()
----> 1 [age][2]

IndexError: list index out of range

Придется сначала доставать первый (и единственный) элемент из [age], а потом внутри него выбирать элемент с индексом 2.

In [23]:
[age][0][2]
Out[23]:
32

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

In [24]:
[1, 2, 3] + [9, 10]
Out[24]:
[1, 2, 3, 9, 10]

Запись через + кажется очень интуитивной и заманчивой, но не стоит ей часто пользоваться, особенно, когда списки большие и когда списков много. При такой конкатенации списков происходит создание нового списка, который «склеивается» из отдельных частей, чего не происходит при использовании extend: там элементы просто дописываются в уже существующий список. Поэтому приписывание одного списка в конец другого быстрее и эффективнее делать именно через extend.

Срезы (slices)

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

In [25]:
print(age) 
[32, 25, 32, 48, 19]
In [26]:
age[1:3] # левый конец включается, а правый нет
Out[26]:
[25, 32]

Важно: правый конец не включается в срез! В срез выше вошли элементы с индексами 1 и 2, элемент с индексом 3 включен не был.

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

In [27]:
print(age[1:])
print(age[3:])
print(age[:2])
[25, 32, 48, 19]
[48, 19]
[32, 25]

Тут мы подходим к тому, почему нумерация элементов в Python начинается с нуля. В частности, для удобных срезов. Если нам нужны первые два элемента списка, нам не нужно долго думать и сдвигать номера элементов на единицу, достаточно просто написать, например, age[:2].

Можно ли сделать срез, который будет включать в себя весь список? Легко!

In [28]:
age[:] # опускаем все индексы
Out[28]:
[32, 25, 32, 48, 19]

Получить пустой срез тоже дело нехитрое: нужно, чтобы индексы начала и конца совпадали.

In [29]:
age[2:2] # пустой срез
Out[29]:
[]

А теперь вопрос. У нас есть такой срез:

In [30]:
age[:2]
Out[30]:
[32, 25]

Какой срез к нему нужно добавить, чтобы получить целый список age?

In [31]:
age[:2] + age[2:] # срез 2:
Out[31]:
[32, 25, 32, 48, 19]

И это будет верно для любого индекса $k$, не только двойки.

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

In [32]:
print(age)

age[1:3] = [25, 26] # заменим 1 и 2 элементы 
print(age)
[32, 25, 32, 48, 19]
[32, 25, 26, 48, 19]

Длина списка, на который мы заменяем срез, не обязательно должна совпадать с длиной среза. Можно взять список с большим числом элементов, тогда исходный список расширится, а можно с меньшим – список сузится. Замены остальных элементов при этом не произойдет, новый срез просто «вклинится» в середину списка.

In [33]:
age[1:3] = [18, 32, 45]
print(age)
[32, 18, 32, 45, 48, 19]
In [34]:
age[1:3] = [18]
print(age)
[32, 18, 45, 48, 19]

Изменение списков

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

In [36]:
L1 = [1, 8, 9, 4]
L2 = L1 # сохранили список L1 в L2

print(L1)
print(L2)
[1, 8, 9, 4]
[1, 8, 9, 4]

Пока все ожидаемо. Теперь изменим элемент списка L2 с индексом 3:

In [37]:
L2[3] = 5
print(L2)
[1, 8, 9, 5]

А теперь посмотрим на список L1.

In [38]:
print(L1)
[1, 8, 9, 5]

Несмотря на то, что список L1 мы не трогали, он изменился точно так же, как и список L2! Что произошло? На самом деле, когда мы записали L2 = L1, мы скопировали не сам список, а ссылку на него. Другими словами, проводя аналогию с папкой и ярлыком, вместо того, чтобы создать новую папку L2 с элементами, такими же, как в L1, мы создали ярлык L2, который сам по себе ничего не представляет, а просто ссылается на папку L1.

Так как же тогда копировать списки? Во-первых, у списков есть метод .copy().

In [39]:
# дубль два
L1 = [1, 8, 9, 4]
L2 = L1.copy()

# теперь делаем что угодно

L2[3] = 100

print(L1)
print(L2) # все нормально
[1, 8, 9, 4]
[1, 8, 9, 100]

Во-вторых, можно сделать срез и «срезать» весь список:

In [40]:
# дубль три

L1 = [1, 8, 9, 4]
L2 = L1[:] # полный срез

# теперь делаем что угодно

L2[3] = 100

print(L1)
print(L2) # все нормально
[1, 8, 9, 4]
[1, 8, 9, 100]

Цикл for

Раз есть списки, хочется научиться пробегаться по их элементам. Например, выводить на экран не весь список age сразу, а постепенно, каждый элемент с новой строчки. Для этого есть циклы. Рассмотрим цикл for.

In [41]:
for i in age:
    print(i)
32
18
45
48
19

Как устроен цикл выше? Кодом выше мы доносим до Python мысль: пробегайся по всем элементам списка age (for i in age) и выводи каждый элемент на экран (print(i)). Вообще любой цикл for имеет такую структуру: сначала указывается, по каким значениям нужно пробегаться, а потом, что нужно делать. Действия, которые нужно выполнить в цикле, указываются после двоеточия в for – эта часть назвается телом цикла.

Буквы в конструкции for могут быть любые, совсем необязательно брать букву i. Python сам поймет, что мы имеем в виду, запуская цикл. Давайте, используя цикл, создадим новый список.

In [42]:
list1 = [1, 3, 5, 9]
list2 = [] # новый список
for l in list1:
    list2.append(l * 2) # добавляем в него значения из list1, умноженные на 2
print(list2)
[2, 6, 10, 18]

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

In [43]:
# создадим список с номерами дней

days = [2, 3, 4, 5, 6, 7, 8, 9 , 10]

# начальное значение времени, которое питон проводит на солнце

time = 1

print(1, time)

# теперь будем изменять значение time в цикле
# и выводить на экран номер дня и время

for d in days:
    time = time + 3
    print(d, time)
1 1
2 4
3 7
4 10
5 13
6 16
7 19
8 22
9 25
10 28

Функция range()

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

In [44]:
# пример

for j in range(0, 6):
    print(j)
0
1
2
3
4
5

Правый конец заданного в range() промежутка не включается, будьте бдительны. В примере выше на экран были выведены числа от 0 до 5, число 6 включено не было. Применим range() к нашей задаче про питона:

In [45]:
time = 1
print(1, time)

for d in range(2, 11):
    time = time + 3
    print(d, time)
1 1
2 4
3 7
4 10
5 13
6 16
7 19
8 22
9 25
10 28

Если мы хотим посмотреть на то, какие значения будут в range(), придется превратить его в список:

In [46]:
range(0, 3) # не поспоришь, но бесполезно
Out[46]:
range(0, 3)
In [47]:
list(range(0, 3)) # значения внутри range
Out[47]:
[0, 1, 2]

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

In [48]:
list(range(5))
Out[48]:
[0, 1, 2, 3, 4]