#!/usr/bin/env python # coding: utf-8 # # Компьютерные инструменты обработки данных # # *Текст лекции: Щуров И.В., НИУ ВШЭ* # # Данный notebook является конспектом лекции по курсу «Компьютерные инструменты обработки данных» (Совместный бакалавриат НИУ ВШЭ и РЭШ, 2015-16). Он распространяется на условиях лицензии [Creative Commons Attribution-Share Alike 4.0](http://creativecommons.org/licenses/by-sa/4.0/). При использовании обязательно упоминание автора курса и аффилиации. При наличии технической возможности необходимо также указать активную гиперссылку на [страницу курса](http://math-info.hse.ru/s15/19). Фрагменты кода, включенные в этот notebook, публикуются как [общественное достояние](http://creativecommons.org/publicdomain/zero/1.0/). # # Другие материалы курса, включая конспекты и видеозаписи лекций, а также наборы задач, можно найти на [странице курса](http://math-info.hse.ru/s15/19). # ### Проверка условий # В ходе выполнения программы иногда требуется в зависимости от каких-то условий выполнять тот или иной фрагмент кода. Например, если пользователь ввёл не те данные, которые от него просили (хотели положительное число, а получили отрицательное), то надо вывести ошибку и попросить ввести данные снова. Решение этой задачи разбивается на несколько шагов: сначала нужно проверить некоторое условие, а потом в зависимости от результата этой проверки выбрать, какой код выполнять. Давайте начнём с проверки условий. # In[1]: 6 < 8 # Здесь мы спросили «Правда ли, что 6 меньше 8?». «Воистину так» — ответил Python на своём заморском языке. Слово `True`, которое он выдал — это не просто слово, означающее «истина», а специальное логическое значение. Его ещё называют «булевским» (по имени одного из основателей математической логики [Джоджа Буля](https://ru.wikipedia.org/wiki/%D0%91%D1%83%D0%BB%D1%8C,_%D0%94%D0%B6%D0%BE%D1%80%D0%B4%D0%B6)). Оно бывает всего двух видов: либо истина (`True`), либо ложь (`False`). Третьего не дано. # In[2]: 8 > 9 # Результат проверки можно записать в переменную. # In[3]: condition = 6 < 8 # In[4]: condition # Говорят, что переменная `condition` теперь булевская (`bool`). # In[5]: type(condition) # Можно проверять равенство двух величин. Правда ли, что 7 равно 7? # In[6]: 7 == 7 # Обратите внимание: здесь нужно написать символ равенства два раза, потому что один знак равно — это операция присвоения («присвоить то, что справа, тому, что слева»), а операция проверки равенства — это совсем другая штука. Например. # In[7]: a = 5 # Положили в `a` число `5`. Такая операция ничего не вернула. # In[8]: a = 7 # Теперь положили в `a` число 7. # In[9]: a == 5 # Теперь спросили, правда ли, что `a` равняется пяти. Получили `False`. # # Надо сказать, что сравнение работает достаточно разумным образом. Например, число `7` и число `7.0` — это, строго говоря, разные объекты (первое — это целое число, второе — число с плавающей запятой), но понятно, что как числа это один и тот же объект. Поэтому сравнение выдаст `True`. # In[10]: 7 == 7.0 # ### Оператор if # Хорошо, мы научились проверять разнообразные условия. Теперь нужно менять поведение программы в зависимости от результатов такой проверки. Например, мы хотим ввести число с клавиатуры и в случае, если оно оказалось отрицательным, сообщить об ошибке. Для этого нужно использовать конструкцию `if`. # In[11]: a = int(input("Введите положительное число: ")) if a < 0: print("Ошибка!") print("Число не является положительным!") print("Вы ввели", a) # Нужно обратить внимание на несколько вещей: во-первых, после `if` указывается условие, а после условия обязательно ставится двоеточие (как и в циклах), дальше идёт блок команд, которые выполняются в том случае, если условие верно (то есть является `True`). Как и в циклах, этот блок команд должен быть выделен отступом. Команды, не входящие в блок (в данном случае это последняя строчка) выполняются в любом случае. # Допустим, мы хотим обработать отдельно обе ситуации: когда условие выполняется и когда оно не выполняется. Для этого нужно использовать ключевое слово `else`. # In[12]: a = int(input("Введите положительное число: ")) if a < 0: print("Ошибка!") print("Число не является положительным!") else: print("Как хорошо!") print("Вы ввели положительное число!") print("Вы ввели", a) # Конструкция `if-else` работает как альтернатива: выполняется либо один фрагмент кода (после `if` — если условие верно), либо другой (после `else` — если неверно). Иногда нужно проверить несколько условий подряд. # In[13]: a = int(input("Введите какое-нибудь число: ")) if a > 100: print("Это очень большое число") elif a > 10: print("Это больше число") else: print("Это маленькое число") # Здесь используется ключевое слово `elif`, являющееся объединением слов `else` и `if`. Логика такая: сначала выполняется первое условие (`a > 100`), если оно верно, то выполняется код после `if`, если неверно, то проверяется следующее условие (`a > 10`), если оно верно, то выполняется код после `elif`, если неверно, то выполняется код после `else`. Команда `else`, если она есть, всегда должна идти в конце. Блоков `elif` может быть много. Условия проверяются по очереди, начиная от первого; как только какое-то из условий оказывается верным, выполняется соответствующий блок и проверка остальных условий не производится. # ### Сложные условия # Допустим, нам нужно проверить выполнение нескольких условий. Скажем, мы хотим получить число от 0 до 100 — числа меньше 0 или больше 100 нас не устраивают. Это можно было бы сделать с помощью нескольких вложенных операторов `if` примерно так. # In[14]: a = int(input("Пожалуйста, введите число от 0 до 100: ")) if a <= 100: if a >= 0: print("Спасибо, мне нравится ваше число") else: print("Вы ошиблись, это не число от 0 до 100") else: print("Вы ошиблись, это не число от 0 до 100") # Этот код довольно громоздок, строчку с сообщением об ошибке пришлось скопировать дважды. Не очень хорошо. Оказывается, можно реализовать тот же функционал проще. # In[15]: a = int(input("Пожалуйста, введите число от 0 до 100: ")) if a <= 100 and a >= 0: print("Спасибо, мне нравится ваше число") else: print("Вы ошиблись, это не число от 0 до 100") # Здесь используется ключевое слово `and`, обозначающее операцию *логического И*. Оно делает следующее: проверяет левое условие (в данном случае `a <= 100`), проверяет правое условие (`a >= 100`) и если оба этих условия выполняются (то есть имеют значение `True`), то и результат выполнения `and` оказывается `True`; если же хотя бы одно из них не выполняется (то есть имеет значение `False`), то и результат выполнения `and` является `False`. Таким образом мы можем проверить в точности интересующее нас условие. # # > Строго говоря, если левый аргумент `and` оказывается ложью, то правый даже не вычисляется: зачем тратить время, если уже понятно, что возвращать надо ложь? # Можно было бы переписать этот код другим способом, используя логическое ИЛИ (`or`): # In[16]: a = int(input("Пожалуйста, введите число от 0 до 100: ")) if a > 100 or a < 0: print("Вы ошиблись, это не число от 0 до 100") else: print("Спасибо, мне нравится ваше число") # Результат выполнения `or` является истиной в том случае, если хотя бы один аргумент является истиной. Наконец, есть третий логический оператор — это отрицание (`not`). Он имеет всего один аргумент и возвращает истину, если этот аргумент является ложью, и наоборот. # In[17]: a = int(input("Пожалуйста, введите число от 0 до 100: ")) if not (a <= 100 and a >= 0): print("Вы ошиблись, это не число от 0 до 100") else: print("Спасибо, мне нравится ваше число") # Можно проверить, как работают логические команды, просто подставляя в качестве аргументов `True` или `False`: # In[18]: True or False # In[19]: False and True # Можно даже задать Python известный вопрос: быть или не быть? # In[20]: to_be = False to_be or not to_be # Что будет, если `to_be` сделать равным `True`? # ## Лекция 2. Списки и цикл for # ### Списки и операции с ними # До сих пор мы работали с числовыми и строковыми переменными — в каждой переменной лежало одно число или одна строка. На практике нам зачастую приходится работать с большими массивами данных. Данные бывают разные и хранятся в разных *структурах*. Мы начнём с самой просторой структуры данных — со *списков*. Список — это такая структура данных, которая содержит в себе сразу много элементов. # In[21]: numbers = [4, 8, 9, 2, 6] # вот эта штука в квадратных скобках — это и есть список # In[22]: numbers # В списках можно хранить не только числа. Например, создадим список из строк. # In[23]: strings = ["Hello", "World", "Test"] # В списке могут храниться данные разных типов. Например, строки, целые числа, числа с плавающей точкой. # In[24]: mixed_list = ["Hello", 6, 7.8] # Можно обращаться к отдельным элементам списка и работать с ними как с обычными переменными. Чтобы выбрать элемент нужно указать его номер. # In[25]: print(numbers) print(numbers[1]) # **Внимание!** **Нумерация начинается с нуля!** Это такая старая программистская традиция, чтобы запутать непосвященных. Привыкайте. # > На самом деле, у этого правила есть свои [рациональные обоснования](http://python-history.blogspot.ru/2013/10/why-python-uses-0-based-indexing.html). # Если в списке есть элементы разных типов, они никак не «мешают» друг другу. Например, наличие в списке строк не превращает другие элементы этого списка в строки. # > Это касается только обычных списков Python. Несколько позже мы будем проходить массивы `numpy` и там всё не так. # In[26]: print(mixed_list) print(mixed_list[2]+4) # Элементы списка можно менять так же, как значения обычных переменных. # In[27]: numbers # In[28]: numbers[1]=222 numbers # > Если вы когда-нибудь изучали программирование и знаете, что такое «односвязный список» и «двусвязный список» — в этом месте можете про это временно забыть. Списки Python основаны на стандартных C'шных массивах и обладают их свойствами с точки зрения производительности: в частности, обращение к элементу по его индексу имеет сложность $O(1)$, то есть не является массовой операцией. # # Чтобы узнать длину списка, можно использовать функцю `len`. # In[29]: len(numbers) # Заметим, что это не индекс последнего элемента, а именно число элементов. Если вам нужно получить последний элемент, то его индексом будет `len(numbers)-1`. Но в Python можно обращаться к элементам списка, считая их «с конца», гораздо проще: # In[30]: numbers = [4, 8, 2, 5] numbers[-1] # In[31]: numbers[-2] # А вот если вы попытаетесь обратиться к элементу с несуществующим индексом, то получите ошибку. # In[32]: numbers[5] = 100 # Однако дописывать элементы в конец можно: # In[33]: numbers = [7, 6, 2] print(numbers) numbers.append(777) print(numbers) # Слово `append` — это так называемый «метод» — функция, «принадлежащая» некоторому объекту (в данном случае — объекту `numbers` типа `list` (список)), и что-то делающая с этим объектом. У `numbers`, как у любого списка, есть много методов. Можно набрать `numbers.`, нажать табуляцию (после точки), и получить список доступных методов. А ещё можно набрать `help(list)` или даже `help(numbers)` (в нашем случае) и получить краткое описание этих методов. Например, так можно узнать, что помимо `append` у списков есть метод `extend`. # In[34]: print(numbers) numbers.extend([3, 7, 5]) print(numbers) # Метод `extend` позволяет приписать к списку сразу несколько элементов. Он получает на вход список, который нужно приписать: обратите внимание на квадратные скобки внутри круглых при вызове этого метода — они создают новый список, который и передаётся функции `extend`. # # > Приписывание одного списка к другому называется *конкатенацией*. Это умное слово, которое используют программисты, чтобы произвети впечатление на непосвящённых. Вы теперь тоже так умеете. # # Методы `append` и `extend` меняют список, к которому они применяются. Иногда вместо этого нужно создать новый список, объединив (конкатенировав!) два других. Это тоже можно сделать. # In[35]: first_list = [5, 8, 2] second_list = [1, 9, 4] new_list = first_list + second_list print(new_list) # Плюсик в данном случае обозначает не поэлементное сложение (как вы могли подумать), а конкатенацию. Cписки `first_list` и `second_list` при этом не изменились # In[36]: print(first_list) print(second_list) # У вас могло возникнуть желание использовать сложение вместо операции `extend`. # In[37]: print(numbers) # не надо так numbers = numbers + [2, 6, 9] print(numbers) # Вообще говоря, этот код сработал, но делать так не следует: при выполнении операции конкатенации создаётся новый список, затем в него копируются все элементы из `numbers`, потом к ним приписываются элементы из второго списка, после чего старый `numbers` забывается. Если бы в `numbers` было много элементов, их копирование в новый список заняло бы много времени. Гораздо быстрее приписать элементы к уже готовому списку. # # > Впрочем, операция `+=` для списков, по всей видимости, является эквивалентом для `extend` (хотя мне не удалось сходу найти подтверждение в документации). # ### Срезы # Иногда нам нужен не весь список, а его кусочек. Его можно получить, указав в квадратных скобках не одно число, а два, разделённых двоеточием. # In[38]: print(numbers) print(numbers[1:4]) # Это называется *slice* (по-русски часто говорят *срез*). Обратите внимание: левый конец среза включается (элемент с индексом 1 — это шестёрка), а правый — нет. Так будет всегда. Это соглашение оказывается удобным, например, потому что позволяет посчитать число элементов в срезе — нужно из правого конца вычесть левый (в данном случае 4-1=3). # # Если левый элемент не указан, то он считается началом списка, а если правый — то концом. # In[39]: print(numbers[7:]) print(numbers[:7]) # Всегда верно следующее: список `numbers` это то же самое, что `numbers[:k]+numbers[k:]`, где `k` — любой индекс. # Срезы можно использовать для присваивания. # In[40]: numbers = [5, 8, 9, 10] print(numbers[1:3]) numbers[1:3]= [55, 77] print(numbers) # Не обязательно, чтобы список, который мы присваиваем срезу, имел ту же длину, что и срез. Можно присвоить более длинный список (тогда исходный список расширится), а можно менее длинный (тогда сузится). Можно использовать срезы, чтобы вставить несколько элементов внутрь списка. (Для одного элемента это можно делать с помощью метода `insert`.) # In[41]: numbers = [6, 8, 9] print(numbers[1:1]) # это пустой срез numbers[1:1] = [99, 77, 55] print(numbers) # > Чтобы вставить какие-то элементы внутрь списка, необходимо освободить для него место, сдвинув все последующие элементы вперёд. Python сделает это автоматически, но время это займёт. Поэтому, по возможности, следует этого избегать, особенно если вы работаете с большими массивами данных. Если вам очень нужно записывать что-нибудь в начало и конец списка, посмотрите на двустороннюю очередь (deque) из модуля `collections`. # Можно удалять элементы списка (и вообще что угодно) или срезы с помощью команды `del`. # In[42]: numbers = [6, 7, 9, 12, 8, 3] del(numbers[4]) # удалим 8 print(numbers) del(numbers[0:2]) print(numbers) # ### Присвоение и копирование списков # Списки могут быть коварными. Пока вы не разберётесь с содержанием этого раздела, ваши программы будут вести себя непредсказуемым образом и вы потратите много времени на их отладку. Так что сейчас самое время сосредоточиться. # In[43]: first_list = [5, 8, 9, 'Hello'] second_list = first_list # In[44]: first_list # In[45]: second_list # Так мы создали два одинаковым списка. Изменим теперь один из них: # In[46]: second_list[0] = 777 second_list # Что вы ожидаете увидеть в `first_list`? # In[47]: first_list # **Ой!** Когда мы изменили список `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](http://pythontutor.com/), с помощью которого можно визуализировать выполнение кода. (Вы можете использовать этот сайт для отладки своих программ.) # In[48]: get_ipython().run_line_magic('load_ext', 'tutormagic') # Это магия, позволяющая вставить визуализацию с pythontutor прямо в этот notebook. # Чтобы его использовать, необходимо установить пакет tutormagic # pip install tutormagic # In[49]: get_ipython().run_cell_magic('tutor', '--lang python3 # магия', "first_list = [5, 8, 9, 'Hello']\nsecond_list = first_list\nsecond_list[0] = 777\n") # В левой части наш код, зелёная стрелка — это команда, которая только что была выполнена, красная — это команда, которую сейчас предстоит выполнить; в правой части — мир имён (Frames) и мир платоновских идеальных объектов (Objects). Возможно, вам придётся воспользоваться горизонатльной прокруткой, чтобы увидеть платоновский мир. Нажимая на кнопку Forward, вы можете проследить, что происходит с вашим кодом. # Если мы хотим создать действительно новый список, то есть *скопировать* существующий, нужно использовать метод `copy()`. # In[50]: first_list = [6, 9, 2, 5] third_list = first_list.copy() print(third_list) third_list[0] = 100 print(third_list) print(first_list) # Как видите, теперь `first_list` и `third_list` ведут себя независимо. Этот код тоже можно визуализировать. # In[51]: get_ipython().run_cell_magic('tutor', '--lang python3', 'first_list = [6, 9, 2, 5]\nthird_list = first_list.copy()\nprint(third_list)\nthird_list[0] = 100\nprint(third_list)\nprint(first_list)\n') # Вы также можете встретиться с таким синтаксисом для копирования списков: # In[52]: first_list = [6, 9, 2, 5] other_list = first_list[:] # Он тоже сработает (по крайней мере, для обычных списков). Здесь `[:]` — это не смайлик, а срез, начало которого совпадает с началом исходного списка, а конец — с концом. Такой код вы часто можете встретить в программах, написанных на Python 2, потому что там не было метода `copy()`.