Программирование на языке Python для сбора и анализа данных

Текст лекции: Щуров И.В., НИУ ВШЭ

Данный notebook является конспектом лекции по курсу «Программирование на языке Python для сбора и анализа данных» (НИУ ВШЭ, 2015-16). Он распространяется на условиях лицензии Creative Commons Attribution-Share Alike 4.0. При использовании обязательно упоминание автора курса и аффилиации. При наличии технической возможности необходимо также указать активную гиперссылку на страницу курса. Фрагменты кода, включенные в этот notebook, публикуются как общественное достояние.

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

Лекция 2. Списки и цикл for

Списки и операции с ними

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

In [1]:
numbers = [4, 8, 9, 2, 6]
# вот эта штука в квадратных скобках — это и есть список
In [2]:
numbers
Out[2]:
[4, 8, 9, 2, 6]

В списках можно хранить не только числа. Например, создадим список из строк.

In [3]:
strings = ["Hello", "World", "Test"]

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

In [4]:
mixed_list = ["Hello", 6, 7.8]

Можно обращаться к отдельным элементам списка и работать с ними как с обычными переменными. Чтобы выбрать элемент нужно указать его номер.

In [5]:
print(numbers)
print(numbers[1])
[4, 8, 9, 2, 6]
8

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

На самом деле, у этого правила есть свои рациональные обоснования.

Если в списке есть элементы разных типов, они никак не «мешают» друг другу. Например, наличие в списке строк не превращает другие элементы этого списка в строки.

Это касается только обычных списков Python. Несколько позже мы будем проходить массивы numpy и там всё не так.

In [6]:
print(mixed_list)
print(mixed_list[2]+4)
['Hello', 6, 7.8]
11.8

Элементы списка можно менять так же, как значения обычных переменных.

In [7]:
numbers
Out[7]:
[4, 8, 9, 2, 6]
In [8]:
numbers[1]=222
numbers
Out[8]:
[4, 222, 9, 2, 6]

Если вы когда-нибудь изучали программирование и знаете, что такое «односвязный список» и «двусвязный список» — в этом месте можете про это временно забыть. Списки Python основаны на стандартных C'шных массивах и обладают их свойствами с точки зрения производительности: в частности, обращение к элементу по его индексу имеет сложность $O(1)$, то есть не является массовой операцией.

Чтобы узнать длину списка, можно использовать функцю len.

In [9]:
len(numbers)
Out[9]:
5

Заметим, что это не индекс последнего элемента, а именно число элементов. Если вам нужно получить последний элемент, то его индексом будет len(numbers)-1. Но в Python можно обращаться к элементам списка, считая их «с конца», гораздо проще:

In [10]:
numbers = [4, 8, 2, 5]
numbers[-1]
Out[10]:
5
In [11]:
numbers[-2]
Out[11]:
2

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

In [12]:
numbers[5] = 100
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-12-0a3abd230a25> in <module>()
----> 1 numbers[5] = 100

IndexError: list assignment index out of range

Однако дописывать элементы в конец можно:

In [13]:
numbers = [7, 6, 2]
print(numbers)
numbers.append(777)
print(numbers)
[7, 6, 2]
[7, 6, 2, 777]

Слово append — это так называемый «метод» — функция, «принадлежащая» некоторому объекту (в данном случае — объекту numbers типа list (список)), и что-то делающая с этим объектом. У numbers, как у любого списка, есть много методов. Можно набрать numbers., нажать табуляцию (после точки), и получить список доступных методов. А ещё можно набрать help(list) или даже help(numbers) (в нашем случае) и получить краткое описание этих методов. Например, так можно узнать, что помимо append у списков есть метод extend.

In [14]:
print(numbers)
numbers.extend([3, 7, 5])
print(numbers)
[7, 6, 2, 777]
[7, 6, 2, 777, 3, 7, 5]

Метод extend позволяет приписать к списку сразу несколько элементов. Он получает на вход список, который нужно приписать: обратите внимание на квадратные скобки внутри круглых при вызове этого метода — они создают новый список, который и передаётся функции extend.

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

Методы append и extend меняют список, к которому они применяются. Иногда вместо этого нужно создать новый список, объединив (конкатенировав!) два других. Это тоже можно сделать.

In [15]:
first_list = [5, 8, 2]
second_list = [1, 9, 4]
new_list = first_list + second_list
print(new_list)
[5, 8, 2, 1, 9, 4]

Плюсик в данном случае обозначает не поэлементное сложение (как вы могли подумать), а конкатенацию. Cписки first_list и second_list при этом не изменились

In [16]:
print(first_list)
print(second_list)
[5, 8, 2]
[1, 9, 4]

У вас могло возникнуть желание использовать сложение вместо операции extend.

In [17]:
print(numbers)
# не надо так
numbers = numbers + [2, 6, 9]
print(numbers)
[7, 6, 2, 777, 3, 7, 5]
[7, 6, 2, 777, 3, 7, 5, 2, 6, 9]

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

Впрочем, операция += для списков, по всей видимости, является эквивалентом для extend (хотя мне не удалось сходу найти подтверждение в документации).

Срезы

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

In [18]:
print(numbers)
print(numbers[1:4])
[7, 6, 2, 777, 3, 7, 5, 2, 6, 9]
[6, 2, 777]

Это называется slice (по-русски часто говорят срез). Обратите внимание: левый конец среза включается (элемент с индексом 1 — это шестёрка), а правый — нет. Так будет всегда. Это соглашение оказывается удобным, например, потому что позволяет посчитать число элементов в срезе — нужно из правого конца вычесть левый (в данном случае 4-1=3).

Если левый элемент не указан, то он считается началом списка, а если правый — то концом.

In [19]:
print(numbers[7:])
print(numbers[:7])
[2, 6, 9]
[7, 6, 2, 777, 3, 7, 5]

Всегда верно следующее: список numbers это то же самое, что numbers[:k]+numbers[k:], где k — любой индекс.

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

In [20]:
numbers = [5, 8, 9, 10]
print(numbers[1:3])
numbers[1:3]= [55, 77]
print(numbers)
[8, 9]
[5, 55, 77, 10]

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

In [21]:
numbers = [6, 8, 9]
print(numbers[1:1])
# это пустой срез

numbers[1:1] = [99, 77, 55]
print(numbers)
[]
[6, 99, 77, 55, 8, 9]

Чтобы вставить какие-то элементы внутрь списка, необходимо освободить для него место, сдвинув все последующие элементы вперёд. Python сделает это автоматически, но время это займёт. Поэтому, по возможности, следует этого избегать, особенно если вы работаете с большими массивами данных. Если вам очень нужно записывать что-нибудь в начало и конец списка, посмотрите на двустороннюю очередь (deque) из модуля collections.

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

In [22]:
numbers = [6, 7, 9, 12, 8, 3]
del(numbers[4]) # удалим 8
print(numbers)
del(numbers[0:2])
print(numbers)
[6, 7, 9, 12, 3]
[9, 12, 3]

Присвоение и копирование списков

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

In [23]:
first_list = [5, 8, 9, 'Hello']
second_list = first_list
In [24]:
first_list
Out[24]:
[5, 8, 9, 'Hello']
In [25]:
second_list
Out[25]:
[5, 8, 9, 'Hello']

Так мы создали два одинаковым списка. Изменим теперь один из них:

In [26]:
second_list[0] = 777
second_list
Out[26]:
[777, 8, 9, 'Hello']

Что вы ожидаете увидеть в first_list?

In [27]:
first_list
Out[27]:
[777, 8, 9, 'Hello']

Ой! Когда мы изменили список second_list, магическим образом изменился и исходный список first_list! Почему так произошло? Дело в том, что списки живут в своём собственном мире платоновских идеальных списков. Когда мы присваиваем список переменной, то есть пишем что-нибудь вроде

first_list = [5, 8, 9, 'Hello']

мы делаем две вещи: во-первых, создаём список (с помощью операции «квадратные скобки»), а потом говорим, что теперь переменная first_list будет указывать на этот список (с помощью операции «равно»). Можно сказать, что мы создали список и дали ему имя first_list.

Отныне предлагаю читать знак «=» как «наречём».

После этого в first_list хранится не сам список, а указатель (ссылка) на него. Когда мы присваиваем значение first_list новой переменной second_list, мы не производим копирование списка, мы копируем только указатель. То есть second_list просто стала другим именем для того же самого списка, что и firt_list. Поэтому изменение элементов second_list приведет к изменению first_list, и наоборот.

Чтобы разобраться в происходящем более подробно, посмотрим, что происходит с нашим кодом строчка за строчкой. Для этого я буду использовать сервис Python Tutor, с помощью которого можно визуализировать выполнение кода. (Вы можете использовать этот сайт для отладки своих программ.)

In [28]:
%load_ext tutormagic
# Это магия, позволяющая вставить визуализацию с pythontutor прямо в этот notebook. 
# Чтобы его использовать, необходимо установить пакет tutormagic
# pip install tutormagic
In [29]:
%%tutor --lang python3 # магия
first_list = [5, 8, 9, 'Hello']
second_list = first_list
second_list[0] = 777

В левой части наш код, зелёная стрелка — это команда, которая только что была выполнена, красная — это команда, которую сейчас предстоит выполнить; в правой части — мир имён (Frames) и мир платоновских идеальных объектов (Objects). Возможно, вам придётся воспользоваться горизонатльной прокруткой, чтобы увидеть платоновский мир. Нажимая на кнопку Forward, вы можете проследить, что происходит с вашим кодом.

Если мы хотим создать действительно новый список, то есть скопировать существующий, нужно использовать метод copy().

In [30]:
first_list = [6, 9, 2, 5]
third_list = first_list.copy()
print(third_list)
third_list[0] = 100
print(third_list)
print(first_list)
[6, 9, 2, 5]
[100, 9, 2, 5]
[6, 9, 2, 5]

Как видите, теперь first_list и third_list ведут себя независимо. Этот код тоже можно визуализировать.

In [31]:
%%tutor --lang python3
first_list = [6, 9, 2, 5]
third_list = first_list.copy()
print(third_list)
third_list[0] = 100
print(third_list)
print(first_list)

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

In [32]:
first_list = [6, 9, 2, 5]
other_list = first_list[:]

Он тоже сработает (по крайней мере, для обычных списков). Здесь [:] — это не смайлик, а срез, начало которого совпадает с началом исходного списка, а конец — с концом. Такой код вы часто можете встретить в программах, написанных на Python 2, потому что там не было метода copy().

Цикл for

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

In [33]:
numbers = [4, 9, 1, 5]
for x in numbers:
    y = x + 1
    print(y)
print("Вот и всё")
print(numbers)
5
10
2
6
Вот и всё
[4, 9, 1, 5]

Здесь требуются пояснения. В первой строчке создаётся список numbers. Во второй используется ключевое слово for, которое переводится с английского для. Читать эту строчку нужно так: «для каждого элемента x из списка numbers сделать то-то и то-то». В конце этой строчки обязательно нужно поставить двоеточие, а в следующей строке сделать отступ. (IPython Notebook сделает его сам.)

С отступами отдельная история. Как правило, нам нужно выполнить в цикле не одну команду, а несколько. Чтобы отметить, где начинается и, самое главное, где заканчивается набор команд, относящихся к циклу (или другой управляющей конструкции), в разных языках программирования используются разные маркеры: в Pascal это ключевые слова begin и end, в C-подобных языках это фигурные скоби. Наряду с этими маркерами, во всех языках программирования есть правило хорошего тона: начал блок команд — сделай отступ. Это нужно, чтобы сразу видеть, где блок заканчивается, и какие команды к каким блокам относятся. Однако, компиляторы ориентировались при разборе программы как раз на маркеры, а не на отступы, и в результате правило про отступы можно было смело игнорировать: копилятор всё равно поймёт. Другие разработчики, правда, не поймут, но… В общем, правило про отступы нередко нарушалось, особенно начинающими программистами. Автор Python решил, что в его языке это правило будут соблюдать все, и поэтому просто не оставил других способов выделить блок, кроме отступов.

Итак, отступами выделено тело цикла, в него входят в данном случае две команды. Когда Python доходит до строчки for, он берёт первый элемент списка (в данном случае numbers), присваивает его соответствующей переменной (в данном случае x), после чего идёт выполнять строчки из тела цикла. Когда он дойдёт до конца тела цикла, он вернётся к его началу, возьмёт следующий элемент списка (если он есть), присвоит теперь его переменной x и снова выполнит тело цикла. Так будет продолжаться до тех пор, пока в списке есть элементы, которые мы не обработали — когда список кончится, цикл прекратится и будут выполнены строчки, которые идут после тела цикла (без отступа).

Иными словами, код выше эквивалентен вот такому коду:

In [34]:
numbers = [4, 9, 1, 5]

# делай раз
x = numbers[0]
y = x + 1
print(y)

# делай два
x = numbers[1]
y = x + 1
print(y)

# делай три
x = numbers[2]
y = x + 1
print(y)

# делай четыре
x = numbers[3]
y = x + 1
print(y)

# закончили упражнение

print("Вот и всё")
print(numbers)
5
10
2
6
Вот и всё
[4, 9, 1, 5]

Можно воспользоваться Python Tutor и визуализировать этот процесс.

In [35]:
%%tutor --lang python3
numbers = [4, 9, 1, 5]
for x in numbers:
    y = x + 1
    print(y)
print("Вот и всё")
print(numbers)

Иногда у нас нет никакого конкретного списка, а просто нужно выполнить некоторый код несколько раз, причём заранее неизвестно, сколько. Для решения этой задачи служит объект range().

In [36]:
for i in range(5):
    print("Hello, i =", i)
Hello, i = 0
Hello, i = 1
Hello, i = 2
Hello, i = 3
Hello, i = 4

range(n) ведёт себя как как список, содержащий целые числа от 0 до n-1 (опять последний элемент не включается!). В нём ровно n элементов. Ещё можно использовать range в двумя аргументами: указать начало и конец интервала: range(3,9). Можно даже превратить range в настоящий список с помощью команды list:

In [37]:
list(range(5))
Out[37]:
[0, 1, 2, 3, 4]
In [38]:
list(range(3,9))
Out[38]:
[3, 4, 5, 6, 7, 8]