Программирование и научные вычисления на языке Python/§3
В следующих двух уроках мы познакомимся с основными конструкциями программирования: циклами while и for, if-else-ветвлениями и функциями, определяемыми самими пользователями. Все это будет использоваться практически в любой программе. Цель нашей следующей программы — развить наш предыдущий пример о двух температурных шкалах и напечатать на экране таблицу с градусами Цельсия в первом столбце и соответствующими им градусами Фаренгейта во втором столбце:
-20 -4.0 -15 5.0 -10 14.0 -5 23.0 0 32.0 5 41.0 10 50.0 15 59.0 20 68.0 25 77.0 30 86.0 35 95.0 40 104.0
Наивный подход
Конечно, мы можем повторить некоторый код столько раз, сколько нужно с соответствующими значениями переменной, например:
C = -20; F = 9.0/5*C + 32; print C, F
C = -15; F = 9.0/5*C + 32; print C, F
C = -10; F = 9.0/5*C + 32; print C, F
C = -5; F = 9.0/5*C + 32; print C, F
C = 0; F = 9.0/5*C + 32; print C, F
C = 5; F = 9.0/5*C + 32; print C, F
C = 10; F = 9.0/5*C + 32; print C, F
C = 15; F = 9.0/5*C + 32; print C, F
C = 20; F = 9.0/5*C + 32; print C, F
C = 25; F = 9.0/5*C + 32; print C, F
C = 30; F = 9.0/5*C + 32; print C, F
C = 35; F = 9.0/5*C + 32; print C, F
C = 40; F = 9.0/5*C + 32; print C, F
и после запуска получить:
-20 -4.0
-15 5.0
-10 14.0
-5 23.0
0 32.0
5 41.0
10 50.0
15 59.0
20 68.0
25 77.0
30 86.0
35 95.0
40 104.0
Форматирование выглядит криво, но его собственно как такового и не осуществлялось и ситуацию легко изменить с помощью printf-форматирования, что мы покажем далее. Но основная проблема заключается в другом — в том, что нам приходится множество раз повторять идентичные выражения, в то время как компьютер создан как раз для автоматизации скучной работы. В общем, нам пора узнать о циклах (loops) и их двух реализациях в Python: циклах while и циклах for.
Цикл while
Цикл while используется для того, чтобы повторять заданные наборы инструкций столько раз, сколько условие сохраняет истину. Проиллюстрируем это на примере. Задача состоит в создании строк таблицы из значений C и F. Первое значение C равно -20 и с шагом 5 оно движется в сторону роста С пока С ≤ 40. Для каждого C считается соответствующее значение F и так записываются две температуры. Записать все сказанное словами в виде кода можно, например, так:
print '------------------' # верхняя линия таблицы
C = -20 # начальное значение C
dC = 5 # инкремент для C в цикле
while C <= 40: # заголовок цикла с условием
F = (9.0/5)*C + 32 # 1ая инструкция цикла
print C, F # 2ая инструкция цикла
C = C + dC # 3ья инструкция цикла
print '------------------' # нижняя линия (после цикла)
Уже сейчас стоит рассмотреть в прямом смысле слова незаметную, но очень важную часть языка Python — отступы. Как мы видим, после того как мы объявили блок while, задали условие и поставили двоеточие, весь следующий текст записан с определенным отступом. Ширина этого отступа, которой следует придерживаться, равна четырем пробелам.
Многие начинающие программисты Python забывают ставить двоеточие в конце строки с while — это двоеточие существенно и показывает, что начинается блок инструкций цикла. Немногим позже мы увидим, что имеется множество схожих конструкций, открывающихся двоеточием.
Программисты должны полностью понимать, что происходит в программе и быть готовы дать тот же ответ, что и программа, еще до того как она запущена. Давайте обсудим, что происходит в этом коде. Вначале, мы определяем начальное значение температуры в градусах Цельсия: С = -20. Также мы определяем инкремент dC, чтобы добавлять его к C в цикле. Затем мы начинаем цикл с условием С ≤ 40. В первый момент C = -20, -20 ≤ 40, условие выполняется. Пока условие выполняется, цикл выполняет заложенные в нем действия. То есть считает соответствующее F, печатает две температуры и добавляет dC к C.
После этого совершается второй проход по циклу. Вначале мы проверяем правило цикла: теперь С = -15, но выражение все еще остается истинным: -15 ≤ 40. Поэтому выполняются все те же инструкции, следующие после двоеточия. И так до тех пор пока выражение С ≤ 40 истинно. А ложным оно может стать лишь тогда, когда С > 40.
Немного об инкременте
Новичков в программировании иногда смущает следующее выражение:
C = C + dC
Эта строка выглядит некорректно с точки зрения математики, но как программный код не вызывает никаких нареканий, поскольку вначале происходят какие-то действия с правой стороны от знака равенства, а потом уже они присваиваются переменной слева. В нашем случае складываются C и dC, два объекта типа int. Эта операция дает тоже объект типа int и уже он присваивается переменной C. А объект, связанный с этим именем ранее в памяти освобождается, поскольку на него более не ссылается ни одно имя.
Поскольку подобные инкременту операции часто встречаются в программировании, существует более короткий способ записи таких операций, о нем будет неплохо узнать:
C += dC # то же, что C = C + dC
C -= dC # то же, что C = C - dC
C *= dC # то же, что C = C * dC
C /= dC # то же, что C = C / dC
Логические выражения
В нашем примере с циклом while мы использовали условие записанное как C <= 40, которое принимает значения True (истина) или False (ложь). В зависимости от задачи, вы можете использовать и другие условия проверки:
C == 40 # C равно 40
C != 40 # C не равно 40
C >= 40 # C больше или равно 40
C > 40 # C больше 40
C < 40 # C меньше 40
Сравниваться могут не только числа. Возможно любое логическое (булево) сравнение. Также перед выражением может быть поставлено слово not для изменения True на False и наоборот. Также возможно комбинировать булеву логику, как например:
while x > 0 and y <= 1:
print x, y
В этом примере, пока оба выражения истинны, будут печататься x, y. В любом обратном случае цикл завершается. Эта функция называется "логическое и". Более "мягкий" подход - "логическое или" (or), для которого выражение принимает истинное значение, когда хотя бы одно из сравнений дает истину:
while x > 0 or y <= 1:
print x, y
Вот несколько примеров в интерактивном режиме, чтобы потренироваться на логических выражениях и быть с ними более аккуратными, чем это бывает обычно:
>>> x = 0; y = 1.2
>>> x >= 0 and y < 1
False
>>> x >= 0 or y < 1
True
>>> x > 0 or y > 1
True
>>> x > 0 or not y > 1
False
>>> -1 < x <= 0 # -1 < x and x <= 0
True
>>> not (x > 0 or y > 0)
False
Списки
До этого момента все наши переменные могли содержать только одно число, будь оно хоть целым (int), хоть с плавающей точкой (float), хоть комплексным (complex) объектом. Но иногда числа естественным образом группируются. Например, градусы шкалы Цельсия в первом столбце представляют собой группу. Для групп в Python есть специальный тип объектов — list (список), который позволяет организовывать эти группы в последовательности. При этом, с помощью списков мы можем работать как с группой целиком, так и с членами этой группы по отдельности. При этом список может состоять из объектов разного типа, в том числе и из самих списков, что как мы увидим далее, может быть очень удобным.
Для того чтобы создать список чисел из первого столбца нашей таблицы, нам следует записать их через запятую в квадратных скобках, вот так:
C = [-20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40]
Есть, кстати, и более элегантный способ, но не будем забегать вперед. Теперь наша переменная C ссылается на объект типа list, который содержит последовательность 13 элементов. Все элементы в этом случае являются int-объектами.
Любой элемент в списке связан со своим индексом, который определяет позицию элемента в списке. Первый элемент имеет индекс 0, второй элемент — индекс 1 и так далее. Таким образом в списке C имеется 13 элементов с индексами от 0 до 12. Для того, чтобы вызвать какой-то элемент списка, достаточно вызвать его по индексу, например C[3], если мы хотим вызывать -5.
Элементы в списках можно удалять, добавлять, изменять, вставлять и так далее. Делается это с помощью методов. Обратите внимание на эту концепцию, она встретится нам не раз. Методы определяются через точку. Например метод append для нашего списка C запишется как C.append(v), и этот метод добавит элемент v в конец списка С. А метод C.insert(i,v) вставит новый элемент v на позицию i. В интерактивном режиме выглядит это так:
>>> C = [-10, -5, 0, 5, 10, 15, 20, 25, 30] # создаем список
>>> C.append(35) # добавляем в конец новый элемент
>>> C # смотрим список
[-10, -5, 0, 5, 10, 15, 20, 25, 30, 35]
>>> C = C + [40, 45] # способ соединения списков
>>> C
[-10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45]
Но заметим, что, как мы уже знаем, в последнем случае был создан новый список. Сами элементы можно было вставить куда угодно, как мы сейчас сделаем с помощью метода C.insert():
>>> C.insert(0, -15) # вставить новый элемент -15 с индексом 0
>>> C
[-15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45]
С помощью del C[i] мы можем удалять любые элементы, с помощью len(C) считать число элементов. Метод C.index(v) позволяет определить под каким индексом расположен интересующий нас элемент.
>>> del C[2] # удалить третий элемент
>>> C
[-15, -10, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45]
>>> del C[2] # удалить то, что теперь третий элемент
>>> C
[-15, -10, 5, 10, 15, 20, 25, 30, 35, 40, 45]
>>> len(C) # длина списка
11
>>> C.index(10)
3
Для того, чтобы просто узнать имеется ли какой-то элемент в списке используется команда in:
>>> 10 in C # есть ли среди элементов C число 10?
True
Кроме всего прочего Python поддерживает отрицательные индексы. Например, в списке C последний элемент может быть обозначен как С[-1], предпоследний как C[-2] и так далее:
>>> C[-1]
45
>>> C[-2]
40
Также с помощью списков можно сделать компактное присвоение элементов переменным. Например:
>>> somelist = ['book.tex', 'book.log', 'book.pdf']
>>> texfile, logfile, pdf = somelist
>>> texfile
'book.tex'
>>> logfile
'book.log'
>>> pdf
'book.pdf'
При этом, конечно, число переменных слева от знака равенства должно равняться числу элементов списка, иначе случится ошибка.
Заметим, напоследок, различие в функциях и методах. Например, C.append(e) является методом, а len(C) - функцией. Как можно заметить просто визуально, методы записываются через точку. Эта их "префиксность" показывает, что они относятся только к объекту такого типа и производятся только над конкретным экземпляром. Функции же как правило, могут быть использованы для различных типов объектов. При этом каких-то особенных различий в действиях нет. То есть методы по сути те же функции, но привязанные к объекту.
Цикл for
Обычно действия, которые мы производим над одним элементом какой-то последовательности, мы хотели бы произвести и над всеми элементами. Для этого в языках программирования имеется специальная конструкция, называемая цикл for (for loop). Давайте сразу начнем с примера:
degrees = [0, 10, 20, 40, 100]
for C in degrees:
print 'list element: ', C
print 'The degrees list has', len(degrees), 'elements'
Предложение for C in degrees создает цикл для всех элементов списка degrees. При каждом проходе по циклу переменная C действует, как курсор: берет соответствующий элемент из списка degrees, начиная с degrees[0] и подставляет в инструкцию цикла. Потом то же делает с degrees[1] и так далее до degrees[n-1], где n — число элементов в списке, которое мы можем определить функцией len(degrees), что и делаем по окончании цикла.
Заголовок цикла for аналогично заголовку цикла while заканчивается двоеточием, после которого следует тело цикла с отступами в 4 пробела. В данном случае в теле цикла всего одна инструкция.
В итоге на экране видим:
list element: 0
list element: 10
list element: 20
list element: 40
list element: 100
The degrees list has 5 elements
Делаем таблицу
Теперь наши знания о циклах и списках помогут написать программу, выводящую требуемую таблицу. При этом нам будет понятно, что происходит "под капотом".
Cdegrees = [-20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40]
for C in Cdegrees:
F = (9.0 / 5) * C + 32
print C, F
Инструкция print C, F просто печатает значения C и F, а мы бы хотели создать форматированный вывод. Тогда, используя printf-форматирование, запишем окончательный вариант:
Cdegrees = [-20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40]
print ' C F'
for C in Cdegrees:
F = (9.0 / 5) * C + 32
print '%5d %5.1f' % (C, F)
Альтернативные возможности
Итак, наша программа делает то, чего мы хотели в самом начале. Для любой проблемы обычно существует несколько решений. Здесь мы узнаем о нескольких других возможных конструкциях.
while как for
Любой for цикл может быть записан как цикл while. В общем случае for-цикл:
for element in somelist:
<process element>
может быть преобразован в while:
index = 0
while index < len(somelist):
element = somelist[index]
<process element>
index += 1
В том числе и наш пример:
Cdegrees = [-20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40]
index = 0
print ' C F'
while index < len(Cdegrees):
C = Cdegrees[index]
F = (9.0 / 5) * C + 32
print '%5d %5.1f' % (C, F)
index += 1
Столбцы в виде списков
Небольшое изменение нашей программы позволит записывать уже не одну, а обе шкалы в виде списка. Для шкалы Фаренгейта мы можем создать пустой список, в который по мере проходов по циклу будут добавляться соответствующие значения:
Cdegrees = [-20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40]
Fdegrees = []
for C in Cdegrees:
F = (9.0 / 5) * C + 32
Fdegrees.append(F)
И если мы после этого распечатаем список Fdegrees, то получим:
[-4.0, 5.0, 14.0, 23.0, 32.0, 41.0, 50.0, 59.0, 68.0, 77.0, 86.0, 95.0, 104.0]
Циклы со списком индексов
Вместо цикла for по заданным элементам списка мы можем использовать цикл for с генерированным списком индексов. Для этого в Python есть функция range, возвращающая список по порядку идущих чисел:
- range(n) возвращает список [0, 1, 2, ..., n-1]
- range(start, stop, step) возвращает список [start, start+step, start+2*step, ..., start+(n-1)*step], то есть число stop не включается в список, как и ранее. Например, range(2, 8, 3) дает [2, 5]; range(1, 11, 2) возвращает [1, 3, 5, 7, 9].
- range(start, stop) то же самое, что range(start, stop, 1).
Таким образом, рассмотренный цикл for может быть записан и так:
Cdegrees = range(-20, 45, 5)
Fdegrees = [0.0]*len(Cdegrees) # list of 0.0 values
for i in range(len(Cdegrees)):
Fdegrees[i] = (9.0/5)*Cdegrees[i] + 32
Заметим, что в этом примере мы инициализировали Fdegrees списком длиной len(Cdegrees), в котором все элементы равны нулю. Иначе, если бы мы создали пустой список Fdegrees = [], обращение Fdegrees[i] дало бы ошибку, поскольку список пуст, и мест нет.
Еще о списках
Изменение
Сейчас мы могли бы предложить два способа того как можно изменять элементы списка, например:
for c in Cdegrees:
c += 5
и этот цикл ничего не изменяет, в то время как
for i in range(len(Cdegrees)):
Cdegrees[i] += 5
работает исправно. Что же не так с первым циклом? Проблема в том, что с в первом случае это всего лишь переменная, которой было присвоено значение элемента списка, и когда мы к этой переменной применяем выражение c += 5, мы изменяем переменную, но не список. То есть, как говорилось ранее, можно представлять такого рода переменные как курсоры, а действие над указателем не изменяет объект. Показать, что происходит, можно на примере первых двух проходов по циклу:
c = Cdegrees[0]
c += 5
c = Cdegrees[1]
c += 5
Как видно отсюда, переменная c может быть использована только для чтения элементов, но не для их изменения. Только присваивание вида:
Cdegrees[i] = ...
изменяет элемент списка. Существует возможность совмещения действий с индексами и элементами, например, цикл
for i, c in enumerate(Cdegrees):
Cdegrees[i] = c + 5
добавляет число 5 к каждому элементу списка.
Генерация
Поскольку прохождение по списку и создание соответствующего нового списка является часто встречаемой задачей, в Python имеется компактный синтаксис для решения этой проблемы, который часто называют генерацией списка. В общем виде это выглядит так:
newlist = [E(e) for e in list]
где E(e) представляет собой выражение с элементом e. Вот три примера:
Cdegrees = [-5 + i*0.5 for i in range(n)]
Fdegrees = [(9.0/5)*C + 32 for C in Cdegrees]
C_plus_5 = [C+5 for C in Cdegrees]
Двойной проход (zip)
Мы можем использовать списки Cdegrees и Fdegrees для того, чтобы получить таблицу. Для этого мы должны пройти по обеим последовательностям. Конструкция for element in list в этом случае уже не очень удобна, поскольку она извлекает элементы только из одного списка. Поскольку мы знаем, что в каждом списке одно и то же число элементов, можно поступить например так:
for i in range(len(Cdegrees)):
print '%5d %5.1f' % (Cdegrees[i], Fdegrees[i])
Такая задача — о том как одновременно пройти по двум и более спискам встречается довольно часто. В Python для этого есть свое элегантное решение:
for e1, e2, e3, ... in zip(list1, list2, list3, ...):
# работаем с e1 из list1, e2 из list2 и т.д.
Функция zip собирает из n списков (list1, list2, list3, ...) один список из n-последовательностей (кортежей). Теперь каждая переменная e1, e2, e3 и т. д. служит своеобразным курсором для своей последовательности и двигаются параллельно. Цикл останавливается как только достигается конец самого короткого списка. Разъясним применение zip на нашем примере с таблицей из двух списков Cdegrees и Fdegrees:
for C, F in zip(Cdegrees, Fdegrees):
print '%5d %5.1f' % (C, F)
Заметно, что этот подход более естественен, чем конструкции вида for i in range(len(Cdegrees)).
Вложение
В нашей таблице мы используем отдельный список для каждого столбца. Если нам будет нужно n столбцов, то потребуется и n списков. Однако, обычно мы представляем себе какую-то таблицу как единое целое, а не как ряд столбцов или строк. Значит, будет и более естественно использовать единый аргумент для всей таблицы. Для этого можно использовать вложенные списки:
Cdegrees = range(-20, 41, 5) # -20, -15, ..., 35, 40
Fdegrees = [(9.0/5)*C + 32 for C in Cdegrees]
table = [Cdegrees, Fdegrees]
Стоит заметить, что индексация к определенному элементу организуется также. То есть, чтобы обратиться к первому списку, мы напишем table[0], а чтобы обратиться к третьему элементу этого списка, мы приставляем следующий индекс table[0][2]. Все это органично входит в концепцию объектов - table[0] это тоже объект и его можно индексировать как любой объект, содержащий последовательность элементов.
Если идти дальше, то с помощью zip можно организовать и более естественное заполнение таблицы:
table = []
for C, F in zip(Cdegrees, Fdegrees):
table.append([C, F])
а с генерацией списков можно получить совсем короткое:
table = [[C, F] for C, F in zip(Cdegrees, Fdegrees)]
Срезы
В Python имеется прекрасный синтаксис для вырезания частей списков, который называется срез (sublist или slice). Срезы никогда не изменяют исходный список, создавая новый. Рассмотрим примеры. A[i:] это срез, который вырезает список из исходного списка A, начиная с индекса i до конца:
>>> A = [2, 3.5, 8, 10]
>>> A[2:]
[8, 10]
A[i:j] вырезает начиная с элемента i до j-1:
>>> A[1:3]
[3.5, 8]
A[:i] вырежет из старого списка новый список, забрав все элементы вплоть до i-1:
>>> A[:3]
[2, 3.5, 8]
A[1:-1] вырезает все элементы кроме первого и последнего (заметьте, что можно использовать отрицательную индексацию), а A[:] копирует весь список:
>>> A[1:-1]
[3.5, 8]
>>> A[:]
[2, 3.5, 8, 10]
Покажем, что срезы всего лишь копии. Доказательство 1:
>>> l1 = [1, 4, 3]
>>> l2 = l1[:-1]
>>> l2
[1, 4]
>>> l1[0] = 100
>>> l1
[100, 4, 3]
>>> l2
[1, 4]
Доказательство 2:
>>> B = A[:]
>>> C = A
>>> B == A
True
>>> B is A
False
>>> C is A
True
Равенство, которое записывается ==, как мы помним является сравнением значений элементов - если они равны, то возвращается True. Выражение is сравнивает сами объекты, если переменные ссылаются на один и тот же объект в памяти (а не на копию), то возвращается True, в обратном случае False.
Однако, возможно присваивание срезу, что может изменить длину списка и даже сделать его пустым:
>>> # Заменим некоторые элементы:
... a[0:2] = [1, 12]
>>> a
[1, 12, 123, 1234]
>>> # Удалим немного:
... a[0:2] = []
>>> a
[123, 1234]
>>> # Вставим пару:
... a[1:1] = ['bletch', 'xyzzy']
>>> a
[123, 'bletch', 'xyzzy', 1234]
Зачем же нужны срезы? Покажем на примере нашей "табличной" программы:
>>> for C, F in table[Cdegrees.index(10):Cdegrees.index(35)]:
... print '%5.0f %5.1f' % (C, F)
...
10 50.0
15 59.0
20 68.0
25 77.0
30 86.0
Помните, что всякий раз, как вы видите пример, вы должны подумать, понимаете ли вы как он работает.
Больше возможностей, больше примеров
a = [] | создание пустого списка |
a = [1, 4.4, ’run.py’] | создание списка |
a.append(elem) | добавить элемент elem в конец списка |
a + [1,3] | сложение двух списков |
a.insert(i, e) | вставить элемент e перед элементом с индексом i |
a[3] | вызов элемента с индексом 3 |
a[-1] | вызов последнего элемента |
a[1:3] | срез: создание копии из элементов с индексами 1, 2 |
del a[3] | удалить элемент с индексом 3 |
a.pop(3) | удалить элемент с индексом 3, вернув его значение |
a.remove(e) | удалить элемент со значением e |
a.index('run.py') | индекс, соответствующий строковому элементу со значением 'run.py' |
'run.py' in a | узнать, содержится ли элемент в списке a |
a.count(v) | сосчитать сколько элементов списка a имеют значение v |
len(a) | число элементов в списке a |
min(a) | наименьший элемент в a |
max(a) | наибольший элемент в a |
sum(a) | сумма всех элементов a |
sorted(a) | возвращает сортированный список a |
reverse(a) | возвращает "перевернутый" список a |
b[3][0][2] | вызов элемента вложенного списка |
isinstance(a, list) | возвращает True, если объект a является списком |
Кортежи
Кортежи (tuples) очень похожи на списки, но кортежи нельзя изменять. В отличие от списков, кортежи записываются в круглых скобках:
>>> t = (2, 4, 6, 'temp.pdf')
Но во многих случаях скобки можно отбросить:
>>> t = 2, 4, 6, 'temp.pdf'
>>> for element in 'myfile.txt', 'yourfile.txt', 'herfile.txt':
... print element
...
myfile.txt yourfile.txt herfile.txt
Большинство возможностей из списков доступно и в кортежах:
>>> t = t + (-1.0, -2.0)
>>> t
(2, 4, 6, 'temp.pdf', -1.0, -2.0)
>>> t[1]
4
>>> t[2:]
(6, 'temp.pdf', -1.0, -2.0)
>>> 6 in t
True
Но любая попытка изменить содержание кортежа приводит к неудаче:
>>> t[1] = -1
...
TypeError: object does not support item assignment
>>> t.append(0)
...
AttributeError: ’tuple’ object has no attribute ’append’
>>> del t[1]
...
TypeError: object doesn’t support item deletion
Возникает вопрос — зачем нужны кортежи, если списки могут делать большее? Во-первых, из того, что кортеж нельзя изменить, вытекает что их можно использовать в таких случаях — когда нельзя изменять какую-то последовательность. Во-вторых, работа с кортежами происходит быстрее, чем со списками — для неизменяемых объектов требуется меньше памяти. В-третьих, кортежи часто используются в Python в качестве ключей в таком типе объектов как словари, где не могут использоваться списки.
Чему мы научились
Это очень важный урок, в котором мы узнали о двух важных областях: такой общевостребованной части программирования как циклы и об особенных объектах Python как последовательности — списки и кортежи. Без этих вещей не обходится практически ни одна программа на Python. Поэтому будет неплохо потренироваться на упражнениях.