Программирование для всех (основы работы с Python)

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

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

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

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

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

In [1]:
age = [25, 35, 48, 20]
print(age)
[25, 35, 48, 20]

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

In [2]:
name = ["Ann", "Nick", "Ben", "George", "James"]
print(name)
['Ann', 'Nick', 'Ben', 'George', 'James']

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

In [3]:
mixed = [23, 25, "no answer", 32]
print(mixed)
[23, 25, 'no answer', 32]

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

In [4]:
L = [[1, 2, 3], [4, 5]]
print(L)
[[1, 2, 3], [4, 5]]

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

In [5]:
len(age) # четыре элемента
Out[5]:
4

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

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

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

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

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

In [8]:
print(age)
print(age[1])
[25, 35, 48, 20]
35

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

In [9]:
age[5]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-9-6dda8ec0be36> in <module>
----> 1 age[5]

IndexError: list index out of range

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

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

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

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

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

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

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

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

In [14]:
# было 2
x = 2
In [15]:
# стало 3
x = x + 1
print(x)
3

Со списками всё обстоит иначе: мы можем обращаться к уже существующему списку, выбирать в нём элемент и заменять его, не перезаписывая весь список «с нуля». Например, заменим последний элемент списка age на число 30:

In [16]:
age[-1] = 30
print(age) 
[25, 35, 48, 30]

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

In [17]:
age.append(27) # добавили 27
print(age) 
[25, 35, 48, 30, 27]
In [18]:
age.extend([43, 33])  # добавили 43 и 33
print(age) 
[25, 35, 48, 30, 27, 43, 33]

Важный момент: методы .append() и extend(), да и почти все методы, которые затрагивают исходный список, молча вносят изменения в сам список, а не возвращают его обновлённую копию. Возвращают они пустое значение None, поэтому использовать одновременно, например, .append() и = для изменения списка – ошибочное решение:

In [19]:
# якобы добавляем 90 и сохраняем обновленный список в age2

age2 = age.append(90) 
print(age2)  # но нет
None

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

In [20]:
print(age) # до

age.insert(3, 29) # добавили 29 четвертым элементом (индекс 3)

print(age) # после 
[25, 35, 48, 30, 27, 43, 33, 90]
[25, 35, 48, 29, 30, 27, 43, 33, 90]

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

In [21]:
L = [4, 5, 6] + [7, 8, 9] 
print(L)
[4, 5, 6, 7, 8, 9]

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

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

In [22]:
age2 = age  # сохранили список L1 в L2
print(age, age2)
[25, 35, 48, 29, 30, 27, 43, 33, 90] [25, 35, 48, 29, 30, 27, 43, 33, 90]

Пока все ожидаемо. А теперь допишем в age2 элемент 18:

In [24]:
age2.append(18)

И сравним оба списка:

In [25]:
print(age, age2) 
[25, 35, 48, 29, 30, 27, 43, 33, 90, 18] [25, 35, 48, 29, 30, 27, 43, 33, 90, 18]

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

Так как же тогда копировать списки? Можно воспользоваться методом .copy().

In [27]:
age2 = age.copy() 
age2.append(18)

print(age)
print(age2)
[25, 35, 48, 29, 30, 27, 43, 33, 90, 18]
[25, 35, 48, 29, 30, 27, 43, 33, 90, 18, 18]

Срезы (slices)

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

In [28]:
age[2:5]
Out[28]:
[48, 29, 30]

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

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

In [29]:
print(age[2:])
print(age[:5])
[48, 29, 30, 27, 43, 33, 90, 18]
[25, 35, 48, 29, 30]

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

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

In [30]:
age[:] # опускаем все индексы
Out[30]:
[25, 35, 48, 29, 30, 27, 43, 33, 90, 18]

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

In [32]:
print(age)

age[1:3] = [28, 26] # заменим элементы с индексами 1 и 2

print(age)
[25, 35, 48, 29, 30, 27, 43, 33, 90, 18]
[25, 28, 26, 29, 30, 27, 43, 33, 90, 18]

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

In [33]:
age[1:3] = [18, 32, 45]
print(age)
[25, 18, 32, 45, 29, 30, 27, 43, 33, 90, 18]

Цикл for

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

Рассмотрим цикл for. Создадим список nums и последовательно выведем его элементы на экран:

In [34]:
nums = [1, 10, 23, -8, 6] 
In [35]:
for i in nums:
    print(i)
1
10
23
-8
6

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

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

In [36]:
# element вместо i

for element in nums:
    print(element)
1
10
23
-8
6

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

In [38]:
for i in nums:
    print(i, i ** 2)  
1 1
10 100
23 529
-8 64
6 36

Давайте, используя цикл, создадим новый список на основе старого.

In [39]:
squares = [] # пока пустой список
for n in nums:
    squares.append(n ** 2) # постепенно записываем элементы
In [40]:
print(squares)
[1, 100, 529, 64, 36]

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

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

In [41]:
# создадим список с номерами дней
# зафиксируем начальное значение времени t = 1 минута

days = [2, 3, 4, 5, 6, 7] 
t = 1
print(1, t) 

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

for day in days: 
    t = t + 3
    print(day, t)
1 1
2 4
3 7
4 10
5 13
6 16
7 19

Функция range()

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

In [42]:
range(0, 7)
Out[42]:
range(0, 7)

Есть небольшая проблема: из-за того, что список с числами не создаётся и не занимает память, явно элементы внутри range() мы не увидим. Можно преобразовать результат в список:

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

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

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

In [44]:
t = 1
print(1, t) 

for day in range(2, 8): 
    t = t + 3
    print(day, t) 
1 1
2 4
3 7
4 10
5 13
6 16
7 19

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

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

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

In [46]:
# шаг 2, только чётные числа от 0 до 16, исключая 16
list(range(0, 16, 2))
Out[46]:
[0, 2, 4, 6, 8, 10, 12, 14]

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

In [47]:
list(range(16, 0, -2)) 
Out[47]:
[16, 14, 12, 10, 8, 6, 4, 2]

Если сначала указать меньшее значение, то мы получим пустой список. Это происходит потому, что мы даём Python противоречивые указания – range() двигается всегда слева направо, а отрицательный шаг предполагает движение справа налево:

In [48]:
list(range(0, 16, -2)) 
Out[48]:
[]