Текст лекции: Щуров И.В., НИУ ВШЭ
Данный notebook является конспектом лекции по курсу «Компьютерные инструменты обработки данных» (Совместный бакалавриат НИУ ВШЭ и РЭШ, 2015-16). Он распространяется на условиях лицензии Creative Commons Attribution-Share Alike 4.0. При использовании обязательно упоминание автора курса и аффилиации. При наличии технической возможности необходимо также указать активную гиперссылку на страницу курса. Фрагменты кода, включенные в этот notebook, публикуются как общественное достояние.
Другие материалы курса, включая конспекты и видеозаписи лекций, а также наборы задач, можно найти на странице курса.
В ходе выполнения программы иногда требуется в зависимости от каких-то условий выполнять тот или иной фрагмент кода. Например, если пользователь ввёл не те данные, которые от него просили (хотели положительное число, а получили отрицательное), то надо вывести ошибку и попросить ввести данные снова. Решение этой задачи разбивается на несколько шагов: сначала нужно проверить некоторое условие, а потом в зависимости от результата этой проверки выбрать, какой код выполнять. Давайте начнём с проверки условий.
6 < 8
True
Здесь мы спросили «Правда ли, что 6 меньше 8?». «Воистину так» — ответил Python на своём заморском языке. Слово True
, которое он выдал — это не просто слово, означающее «истина», а специальное логическое значение. Его ещё называют «булевским» (по имени одного из основателей математической логики Джоджа Буля). Оно бывает всего двух видов: либо истина (True
), либо ложь (False
). Третьего не дано.
8 > 9
False
Результат проверки можно записать в переменную.
condition = 6 < 8
condition
True
Говорят, что переменная condition
теперь булевская (bool
).
type(condition)
bool
Можно проверять равенство двух величин. Правда ли, что 7 равно 7?
7 == 7
True
Обратите внимание: здесь нужно написать символ равенства два раза, потому что один знак равно — это операция присвоения («присвоить то, что справа, тому, что слева»), а операция проверки равенства — это совсем другая штука. Например.
a = 5
Положили в a
число 5
. Такая операция ничего не вернула.
a = 7
Теперь положили в a
число 7.
a == 5
False
Теперь спросили, правда ли, что a
равняется пяти. Получили False
.
Надо сказать, что сравнение работает достаточно разумным образом. Например, число 7
и число 7.0
— это, строго говоря, разные объекты (первое — это целое число, второе — число с плавающей запятой), но понятно, что как числа это один и тот же объект. Поэтому сравнение выдаст True
.
7 == 7.0
True
Хорошо, мы научились проверять разнообразные условия. Теперь нужно менять поведение программы в зависимости от результатов такой проверки. Например, мы хотим ввести число с клавиатуры и в случае, если оно оказалось отрицательным, сообщить об ошибке. Для этого нужно использовать конструкцию if
.
a = int(input("Введите положительное число: "))
if a < 0:
print("Ошибка!")
print("Число не является положительным!")
print("Вы ввели", a)
Введите положительное число: 12 Вы ввели 12
Нужно обратить внимание на несколько вещей: во-первых, после if
указывается условие, а после условия обязательно ставится двоеточие (как и в циклах), дальше идёт блок команд, которые выполняются в том случае, если условие верно (то есть является True
). Как и в циклах, этот блок команд должен быть выделен отступом. Команды, не входящие в блок (в данном случае это последняя строчка) выполняются в любом случае.
Допустим, мы хотим обработать отдельно обе ситуации: когда условие выполняется и когда оно не выполняется. Для этого нужно использовать ключевое слово else
.
a = int(input("Введите положительное число: "))
if a < 0:
print("Ошибка!")
print("Число не является положительным!")
else:
print("Как хорошо!")
print("Вы ввели положительное число!")
print("Вы ввели", a)
Введите положительное число: 13 Как хорошо! Вы ввели положительное число! Вы ввели 13
Конструкция if-else
работает как альтернатива: выполняется либо один фрагмент кода (после if
— если условие верно), либо другой (после else
— если неверно). Иногда нужно проверить несколько условий подряд.
a = int(input("Введите какое-нибудь число: "))
if a > 100:
print("Это очень большое число")
elif a > 10:
print("Это больше число")
else:
print("Это маленькое число")
Введите какое-нибудь число: 11 Это больше число
Здесь используется ключевое слово elif
, являющееся объединением слов else
и if
. Логика такая: сначала выполняется первое условие (a > 100
), если оно верно, то выполняется код после if
, если неверно, то проверяется следующее условие (a > 10
), если оно верно, то выполняется код после elif
, если неверно, то выполняется код после else
. Команда else
, если она есть, всегда должна идти в конце. Блоков elif
может быть много. Условия проверяются по очереди, начиная от первого; как только какое-то из условий оказывается верным, выполняется соответствующий блок и проверка остальных условий не производится.
Допустим, нам нужно проверить выполнение нескольких условий. Скажем, мы хотим получить число от 0 до 100 — числа меньше 0 или больше 100 нас не устраивают. Это можно было бы сделать с помощью нескольких вложенных операторов if
примерно так.
a = int(input("Пожалуйста, введите число от 0 до 100: "))
if a <= 100:
if a >= 0:
print("Спасибо, мне нравится ваше число")
else:
print("Вы ошиблись, это не число от 0 до 100")
else:
print("Вы ошиблись, это не число от 0 до 100")
Пожалуйста, введите число от 0 до 100: 123 Вы ошиблись, это не число от 0 до 100
Этот код довольно громоздок, строчку с сообщением об ошибке пришлось скопировать дважды. Не очень хорошо. Оказывается, можно реализовать тот же функционал проще.
a = int(input("Пожалуйста, введите число от 0 до 100: "))
if a <= 100 and a >= 0:
print("Спасибо, мне нравится ваше число")
else:
print("Вы ошиблись, это не число от 0 до 100")
Пожалуйста, введите число от 0 до 100: 321 Вы ошиблись, это не число от 0 до 100
Здесь используется ключевое слово and
, обозначающее операцию логического И. Оно делает следующее: проверяет левое условие (в данном случае a <= 100
), проверяет правое условие (a >= 100
) и если оба этих условия выполняются (то есть имеют значение True
), то и результат выполнения and
оказывается True
; если же хотя бы одно из них не выполняется (то есть имеет значение False
), то и результат выполнения and
является False
. Таким образом мы можем проверить в точности интересующее нас условие.
Строго говоря, если левый аргумент
and
оказывается ложью, то правый даже не вычисляется: зачем тратить время, если уже понятно, что возвращать надо ложь?
Можно было бы переписать этот код другим способом, используя логическое ИЛИ (or
):
a = int(input("Пожалуйста, введите число от 0 до 100: "))
if a > 100 or a < 0:
print("Вы ошиблись, это не число от 0 до 100")
else:
print("Спасибо, мне нравится ваше число")
Пожалуйста, введите число от 0 до 100: -2 Вы ошиблись, это не число от 0 до 100
Результат выполнения or
является истиной в том случае, если хотя бы один аргумент является истиной. Наконец, есть третий логический оператор — это отрицание (not
). Он имеет всего один аргумент и возвращает истину, если этот аргумент является ложью, и наоборот.
a = int(input("Пожалуйста, введите число от 0 до 100: "))
if not (a <= 100 and a >= 0):
print("Вы ошиблись, это не число от 0 до 100")
else:
print("Спасибо, мне нравится ваше число")
Пожалуйста, введите число от 0 до 100: -3 Вы ошиблись, это не число от 0 до 100
Можно проверить, как работают логические команды, просто подставляя в качестве аргументов True
или False
:
True or False
True
False and True
False
Можно даже задать Python известный вопрос: быть или не быть?
to_be = False
to_be or not to_be
True
Что будет, если to_be
сделать равным True
?
До сих пор мы работали с числовыми и строковыми переменными — в каждой переменной лежало одно число или одна строка. На практике нам зачастую приходится работать с большими массивами данных. Данные бывают разные и хранятся в разных структурах. Мы начнём с самой просторой структуры данных — со списков. Список — это такая структура данных, которая содержит в себе сразу много элементов.
numbers = [4, 8, 9, 2, 6]
# вот эта штука в квадратных скобках — это и есть список
numbers
[4, 8, 9, 2, 6]
В списках можно хранить не только числа. Например, создадим список из строк.
strings = ["Hello", "World", "Test"]
В списке могут храниться данные разных типов. Например, строки, целые числа, числа с плавающей точкой.
mixed_list = ["Hello", 6, 7.8]
Можно обращаться к отдельным элементам списка и работать с ними как с обычными переменными. Чтобы выбрать элемент нужно указать его номер.
print(numbers)
print(numbers[1])
[4, 8, 9, 2, 6] 8
Внимание! Нумерация начинается с нуля! Это такая старая программистская традиция, чтобы запутать непосвященных. Привыкайте.
На самом деле, у этого правила есть свои рациональные обоснования.
Если в списке есть элементы разных типов, они никак не «мешают» друг другу. Например, наличие в списке строк не превращает другие элементы этого списка в строки.
Это касается только обычных списков Python. Несколько позже мы будем проходить массивы
numpy
и там всё не так.
print(mixed_list)
print(mixed_list[2]+4)
['Hello', 6, 7.8] 11.8
Элементы списка можно менять так же, как значения обычных переменных.
numbers
[4, 8, 9, 2, 6]
numbers[1]=222
numbers
[4, 222, 9, 2, 6]
Если вы когда-нибудь изучали программирование и знаете, что такое «односвязный список» и «двусвязный список» — в этом месте можете про это временно забыть. Списки Python основаны на стандартных C'шных массивах и обладают их свойствами с точки зрения производительности: в частности, обращение к элементу по его индексу имеет сложность $O(1)$, то есть не является массовой операцией.
Чтобы узнать длину списка, можно использовать функцю len
.
len(numbers)
5
Заметим, что это не индекс последнего элемента, а именно число элементов. Если вам нужно получить последний элемент, то его индексом будет len(numbers)-1
. Но в Python можно обращаться к элементам списка, считая их «с конца», гораздо проще:
numbers = [4, 8, 2, 5]
numbers[-1]
5
numbers[-2]
2
А вот если вы попытаетесь обратиться к элементу с несуществующим индексом, то получите ошибку.
numbers[5] = 100
--------------------------------------------------------------------------- IndexError Traceback (most recent call last) <ipython-input-32-0a3abd230a25> in <module>() ----> 1 numbers[5] = 100 IndexError: list assignment index out of range
Однако дописывать элементы в конец можно:
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
.
print(numbers)
numbers.extend([3, 7, 5])
print(numbers)
[7, 6, 2, 777] [7, 6, 2, 777, 3, 7, 5]
Метод extend
позволяет приписать к списку сразу несколько элементов. Он получает на вход список, который нужно приписать: обратите внимание на квадратные скобки внутри круглых при вызове этого метода — они создают новый список, который и передаётся функции extend
.
Приписывание одного списка к другому называется конкатенацией. Это умное слово, которое используют программисты, чтобы произвети впечатление на непосвящённых. Вы теперь тоже так умеете.
Методы append
и extend
меняют список, к которому они применяются. Иногда вместо этого нужно создать новый список, объединив (конкатенировав!) два других. Это тоже можно сделать.
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
при этом не изменились
print(first_list)
print(second_list)
[5, 8, 2] [1, 9, 4]
У вас могло возникнуть желание использовать сложение вместо операции extend
.
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
(хотя мне не удалось сходу найти подтверждение в документации).
Иногда нам нужен не весь список, а его кусочек. Его можно получить, указав в квадратных скобках не одно число, а два, разделённых двоеточием.
print(numbers)
print(numbers[1:4])
[7, 6, 2, 777, 3, 7, 5, 2, 6, 9] [6, 2, 777]
Это называется slice (по-русски часто говорят срез). Обратите внимание: левый конец среза включается (элемент с индексом 1 — это шестёрка), а правый — нет. Так будет всегда. Это соглашение оказывается удобным, например, потому что позволяет посчитать число элементов в срезе — нужно из правого конца вычесть левый (в данном случае 4-1=3).
Если левый элемент не указан, то он считается началом списка, а если правый — то концом.
print(numbers[7:])
print(numbers[:7])
[2, 6, 9] [7, 6, 2, 777, 3, 7, 5]
Всегда верно следующее: список numbers
это то же самое, что numbers[:k]+numbers[k:]
, где k
— любой индекс.
Срезы можно использовать для присваивания.
numbers = [5, 8, 9, 10]
print(numbers[1:3])
numbers[1:3]= [55, 77]
print(numbers)
[8, 9] [5, 55, 77, 10]
Не обязательно, чтобы список, который мы присваиваем срезу, имел ту же длину, что и срез. Можно присвоить более длинный список (тогда исходный список расширится), а можно менее длинный (тогда сузится). Можно использовать срезы, чтобы вставить несколько элементов внутрь списка. (Для одного элемента это можно делать с помощью метода insert
.)
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
.
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]
Списки могут быть коварными. Пока вы не разберётесь с содержанием этого раздела, ваши программы будут вести себя непредсказуемым образом и вы потратите много времени на их отладку. Так что сейчас самое время сосредоточиться.
first_list = [5, 8, 9, 'Hello']
second_list = first_list
first_list
[5, 8, 9, 'Hello']
second_list
[5, 8, 9, 'Hello']
Так мы создали два одинаковым списка. Изменим теперь один из них:
second_list[0] = 777
second_list
[777, 8, 9, 'Hello']
Что вы ожидаете увидеть в first_list
?
first_list
[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, с помощью которого можно визуализировать выполнение кода. (Вы можете использовать этот сайт для отладки своих программ.)
%load_ext tutormagic
# Это магия, позволяющая вставить визуализацию с pythontutor прямо в этот notebook.
# Чтобы его использовать, необходимо установить пакет tutormagic
# pip install tutormagic
%%tutor --lang python3 # магия
first_list = [5, 8, 9, 'Hello']
second_list = first_list
second_list[0] = 777
В левой части наш код, зелёная стрелка — это команда, которая только что была выполнена, красная — это команда, которую сейчас предстоит выполнить; в правой части — мир имён (Frames) и мир платоновских идеальных объектов (Objects). Возможно, вам придётся воспользоваться горизонатльной прокруткой, чтобы увидеть платоновский мир. Нажимая на кнопку Forward, вы можете проследить, что происходит с вашим кодом.
Если мы хотим создать действительно новый список, то есть скопировать существующий, нужно использовать метод copy()
.
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
ведут себя независимо. Этот код тоже можно визуализировать.
%%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)
Вы также можете встретиться с таким синтаксисом для копирования списков:
first_list = [6, 9, 2, 5]
other_list = first_list[:]
Он тоже сработает (по крайней мере, для обычных списков). Здесь [:]
— это не смайлик, а срез, начало которого совпадает с началом исходного списка, а конец — с концом. Такой код вы часто можете встретить в программах, написанных на Python 2, потому что там не было метода copy()
.