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

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

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

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

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

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

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

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

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

In [2]:
name = ["Ann", "Nick", "Ben", "George", "James"] # имена

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

In [3]:
mix = [23, 25, "no answer", 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

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

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

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

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

Если элемента с интересующим нас индексом в списке нет, 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, как мы уже убедились, равна 5, но нумерация самих элементов начинается с нуля. Поэтому:

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

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

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

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

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

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

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

Для сравнения: объекты, с которыми мы работали ранее, были неизменяемыми. Для того, чтобы перезаписать значение числовой переменной a, нужно было явно задействовать оператор присваивания =:

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

Для того, чтобы перезаписать значение строки тоже без присваивания не обойтись:

In [15]:
s = "Welcome"
In [16]:
s.lower()
print(s)  # пока все по-старому
Welcome
In [17]:
s = s.lower()
print(s)  # после приравнивания изменилось
welcome

Теперь создадим список nums и изменим его элемент:

In [18]:
nums = [1, 5, 8, 9]
In [19]:
nums[1] = 15
nums # заменили второй элемент на 15
Out[19]:
[1, 15, 8, 9]

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

In [20]:
nums.append(10) # добавили 10
nums
Out[20]:
[1, 15, 8, 9, 10]
In [21]:
nums.extend([20, 30]) # добавили 20 и 30
nums
Out[21]:
[1, 15, 8, 9, 10, 20, 30]

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

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

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

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

Срезы (slices)

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

In [23]:
print(nums) 
[1, 15, 8, 9, 10, 20, 30]
In [24]:
nums[1:3] # левый конец включается, а правый нет
Out[24]:
[15, 8]

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

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

In [25]:
print(nums[1:])
print(nums[3:])
print(nums[:2])
[15, 8, 9, 10, 20, 30]
[9, 10, 20, 30]
[1, 15]

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

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

In [26]:
nums[:] # опускаем все индексы
Out[26]:
[1, 15, 8, 9, 10, 20, 30]

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

In [27]:
print(nums)

nums[1:3] = [25, 26] # заменим 1 и 2 элементы 

print(nums)
[1, 15, 8, 9, 10, 20, 30]
[1, 25, 26, 9, 10, 20, 30]

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

In [28]:
nums[1:3] = [18, 32, 45]
print(nums)
[1, 18, 32, 45, 9, 10, 20, 30]

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

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

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

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

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

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

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

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

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

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

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

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

L2[3] = 100

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

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

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

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 [34]:
for i in nums:
    print(i)
1
18
32
45
9
10
20
30

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

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

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

for element in nums:
    print(element)
1
18
32
45
9
10
20
30

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

In [36]:
nums2 = []  # новый список
for i in nums:
    nums2.append(i * 2)# добавляем в него значения из nums, умноженные на 2
In [37]:
nums2
Out[37]:
[2, 36, 64, 90, 18, 20, 40, 60]

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

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]