Программирование и научные вычисления на языке Python/§8
В третьем уроке мы узнали о списках, как об удобном способе хранения табулированных данных. Массив представляет собой объект, близкий к списку, но менее гибкий, а в вычислительном плане более эффективный. Когда мы используем компьютер для математических расчетов, мы часто сталкиваемся с огромным множеством чисел и связанных с ними арифметических операций. Хранение чисел в списках в таких случаях может привести к значительному снижению скорости работы программы, в то время как хранение в виде массивов чисел существенно ускоряет решение. Это может быть не очень важным для примеров этого курса, поскольку мы рассматриваем небольшие программы, работающие и с маленькими объемами данных, которые выдают результат в течение нескольких секунд. Тем не менее, более продвинутые приложения, особенно используемые для расчетов в промышленности и науке, прежде чем дать ответ, могут искать его недели и месяцы. Поэтому любая идея, уменьшающая время получения результата, всегда приветствуется. Однако, стоит сказать, что многие программисты изначально предъявляют слишком большое усердие в увеличении скорости, используя сложные конструкции, приводящие к тому, что программы становится дальше сложно поддерживать и совершенствовать. В первую очередь следует стремиться писать ясные, хорошо структурированные и легкие для понимания программы, а уже после этого, на следующем этапе вам будет гораздо проще разобраться как можно ускорить вычисления. В Python довольно часто самое ясное решение работает быстрее менее ясных.
Этот урок кратко знакомит нас с массивами — как они создаются и как могут использоваться. Работа с массивом обычно заканчивается большим количеством чисел, и довольно трудно понять, что они дают, если просто взглянуть на них. Поэтому такую информацию визуализируют в виде графиков кривых, о чем мы поговорим в следующем уроке. И там мы будем использовать массивы для хранения информации о координатах точек графика. То есть не только массивы требуют визуализации, но и графики требуют для себя массивов.
Векторы
[править]Сейчас мы немного поговорим о векторах с тем предположением, что вы что-то слышали о векторах ранее. Это нам нужно как почва для того, чтобы начать работать с массивами и графиками.
Некоторые математические величины связаны с набором чисел. Например, точка на плоскости имеет две координаты, и , и точка ими и описывается как , где вместо символов можно подставить любые числа. То есть точка описывается в виде группы чисел, заключенных в скобки. Точка в трехмерном пространстве описывается схожим способом или . Когда решаются уравнений с неизвестными, решение дает вам группу из чисел .
Такие величины, как , , могут быть представлены в виде векторов, идущих из начала координат в указанную точку. Например, вектор идет из точки в точку , как и трехмерный вектор идет из в . Для последнего случая удобно ввести -мерное пространство, где вектор идет из в . Векторы, как и массивы, можно визуализировать. На плоскости вектор можно представить в виде стрелки. Два вектора, имеющих одинаковое направление и длину, эквивалентны.
О векторе говорят, что он содержит компонент. Каждое из чисел , , это компоненты вектора. Для того, чтобы записать вектор в Python, мы можем использовать списки или кортежи:
v1 = [x, y]
v2 = (-1, 2)
v3 = (x1, x2, x3)
from math import exp
v4 = [exp(-i*0.1) for i in range(150)]
Здесь v1 и v2 — векторы на плоскости, v3 — вектор в трехмерном пространстве, а v4 — вектор в 150-мерном пространстве, состоящий из 150 значений экспоненциальной функции. Поскольку в Python (и многих других языках) индексация начинается с нуля, то более естественным записывать вектор вместо (x1, x2) как вектор (x0, x1). Это не общепринято в математике, но существенно сближает язык математики и язык программирования, что значительно облегчает понимание и уменьшает число потенциальных ошибок.
Невозможно представить как выглядит 150-мерное пространство. Переход от плоскости к пространству и тот бывает дается тяжело. Но представить как происходит переход к четырех-, пяти-, и-так-далее-мерному вектору в виде списка компонент не составляет труда.
Математические операции над векторами
[править]С тех пор, как векторы были введены как массивы чисел имеющие длину и направление, они тут же оказались очень удобны в геометрии и физике. У скорости машины есть значение и направление, есть ускорение и позиция машины также есть точка, которую, как показано выше, можно представить в виде вектора. Грань треугольника также может быть рассмотрена как линия (стрелка), имеющая направление и длину.
В физике и геометрии, использующей векторы, очень важны применяемые математические операции. Давайте рассмотрим наиболее часто встречаемые операции и действующие математические правила. Для этого возьмем два вектора, (, ) и (, ) и для начала сложим их:
.(8.1)
Для вычитания применяется такое же правило:
.(8.2)
Вектор может быть умножен на число:
(8.3)
и скалярно на вектор, что даст число:
.(8.4)
Также возможно и векторное произведение, но рассматривать его здесь будет долго. Длина вектора определяется:
.(8.5)
Все эти операции можно по аналогии продолжить и на -мерное пространство.
Векторные функции
[править]Кроме операций, о которых мы напомнили себе выше, существуют и другие, играющие существенную роль в математических приложениях и особенно в таких средах как Matlab, Octave, Python и R. Эти операции вы вряд ли найдете в книгах посвященных математике, они относятся исключительно к потребностям, возникающим при программировании массивов. Для каждого элемента вектора, его компоненты, мы можем сопоставить функцию одной переменной , тогда мы можем получить и некоторую векторную функцию, в которой компонентами служат функции компонент. Например, у нас есть вектор . Тогда его векторная функция будет выглядеть как . Например, синус от будет записан: .
Векторное возведение в степень может означать: . Особое векторное произведение («asterix» multiplication) определяется как . В компьютерных вычислениях возможна и операция прибавления скаляра к вектору — число прибавляется к каждому элементу вектора. Возможны и сложные выражения, с которыми мы столкнемся далее.
Снова отметим, что эти функции чаще всего мало имеют отношения к обычной математике векторов, в которой то же складывание вектора и скаляра невозможно, а возведение вектора в квадрат даст число, его длину в квадрате. Эти функции работают поочередно с каждым элементом и результатом функции является уже вектор таких элементов. Такие функции позволяют значительно ускорить работу с массивами, производя одновременно одни и те же действия над всеми элементами.
Использование списков
[править]Представим, у нас есть функция и мы хотим применить ее к числам , , , , . Мы можем составить пар (, ), а можем создать два списка — один со значениями переменной, а другой с соответствующими значениями функции:
def f(x):
... return x**3
...
n = 5 # number of points along the x axis
dx = 1.0/(n-1) # spacing between x points in [0,1]
xlist = [i*dx for i in range(n)]
ylist = [f(x) for x in xlist]
pairs = [[x, y] for x, y in zip(xlist, ylist)]
Здесь для решения задачи мы использовали два приема: генерацию списков и двойной zip-проход по спискам. В списке pairs все элементы представляют собой списки из двух float чисел, в списках xlist и ylist все объекты float. Но список это довольно гибкий объект, и он может содержать объекты любых типов:
mylist = [2, 6.0, 'tmp.ps', [0,1]]
Также мы можем легко изменять, добавлять и удалять новые элементы из любого места списка. Эта гибкость списков делает их очень удобной для программистов, но в случае когда элементы однотипны и их число фиксировано, вместо списков используются массивы. Преимущества массивов в быстроте вычислений, меньшей занимаемой памяти и исключительно обширной математической поддержке таких данных. Поэтому массивы, как вы увидите в этом курсе, на практике (и в крупных математических пакетах) находят такое широкое применение. Списки отныне мы будем применять по назначению — когда нам будет нужно удалять и добавлять элементы и использовать в данных объекты различных типов.
Основы Numerical Python
[править]Объект array может быть рассмотрен как вариант списка, но с учетом следующих допущений и возможностей:
- Все элементы массива представлены одним типом объектов, например целыми, действительными или комплексными числами, что делает их хранение и обработку наиболее эффективным.
- В тот момент, когда создается массив, число его элементов должно быть известно.
- Массивы не являются стандартной частью Python — они требуют специального дополнительного пакета, которым пользуются практически все, кто занимаются научными проектами на Python. Этот пакет называется
Numerical Pythonили еще чащеNumPy, поскольку после его установки вызов осуществляется с помощью обычной инструкции импорта модуля:import numpy. Для того, чтобы установитьNumPy, загрузите его с официального сайта проекта. На этой же странице вы обнаружите еще один пакет, который нам понадобится в дальнейшем —SciPy. - С
numpyширокий круг математических операций может быть решен непосредственно с помощью массивов, таким образом исключается потребность в циклах, проходящих по элементам массива. Это свойство носит названия векторизации (vectorization) или прорисовки. - Массивы с одним индексом также называют векторами. Массивы с двумя индексами используются для создания матриц и представления табличной информации. Массив может содержать практически любое количество индексов, то есть быть -мерным.
Как уже было сказано, после установки пакета, работа с модулем происходит обычным образом:
from numpy import *
Конвертирование списка r в массив a происходит привычным способом, но с помощью импортированной из numpy функции:
a = array(r)
Для того, чтобы создать массив из нулевых элементов используем функцию zeros:
a = zeros(n)
Элементы по умолчанию являются float-объектами, второй аргумент функции позволяет изменить тип объектов, например, на int. Часто бывает нужно создать массив из элементов, равномерно распределенных в интервале [p, q]. Для этого в numpy есть функция linspace:
a = linspace(p, q, n)
Вообще говоря в numpy имеется огромное количество функций и внутренних модулей.
Доступ к элементу осуществляется так же как в списках, например, a[1]. Срезы тоже здесь работают, например срез a[1:-1], извлекает список всех элементов, кроме первого и последнего. Но в отличие от списка, здесь это не копия. Например:
b = a[1:-1]
b[2] = 0.1
изменится и массив a, его элемент a[3]=0.1.
К слову, о срезах
[править]Срез в формате a[i:j:s] выбирает все элементы, начиная с i, заканчивая, но не включая, j с шагом s. Например, срез a[0:-1:2] выбирает каждый второй элемент, кроме последнего. Как и ранее, возможны пропуски аргументов, например a[::4] выберет каждый четвертый элемент. Можно взять и отрицательный шаг, тогда элементы будут идти в обратном порядке.
Задание координат и значений функций
[править]Теперь, когда у нас есть эти простейшие операции, мы можем продолжить пример, в котором мы использовали списки:
>>> from numpy import * >>> x2 = array(xlist) >>> y2 = array(ylist) >>> x2 array([ 0. , 0.25, 0.5 , 0.75, 1. ]) >>> y2 array([ 0. , 0.015625, 0.125 , 0.421875, 1. ])
Вместо того, чтобы сначала создавать список, а потом конвертировать его в массив будет естественным сразу же создавать массив. Координаты, что мы задавали в xlist легко получить в виде массива с помощью функции linspace. Массив для значений мы создадим с помощью zeros, чтобы ему изначально была правильно отведена длина, в соответствии с числом элементов в xlist. Далее мы заполняем его с помощью цикла:
>>> from numpy import * >>> n = len(xlist) >>> x2 = linspace(0, 1, n) >>> y2 = zeros(n) >>> for i in xrange(n): ... y2[i] = f(x2[i]) ... >>> y2 array([ 0. , 0.015625, 0.125 , 0.421875, 1. ])
Заметьте, что в цикле мы используем вместо range другую функцию — xrange. Она является более предпочтительной для (обычно больших) массивов. Также отметим, что для y мы использовали генерацию списка, а для y2 — цикл for, поскольку массив это не список. Из положения можно выйти с помощью конвертирования:
x2 = linspace(0, 1, n)
y2 = array([f(xi) for xi in x2])
Тем не менее, есть лучший вариант, который объясняется далее.
Векторизация
[править]Великолепным преимуществом массивов является то, что они могут обходиться без циклов и функция может применяться, как мы объясняли выше, к самому массиву и производить действия над всеми элементами:
>>> y2 = f(x2) >>> y2 array([ 0. , 0.015625, 0.125 , 0.421875, 1. ])
И даже сложные составные выражения
r = sin(x)*cos(x)*exp(-x**2) + 2 + x**2
подвластны волшебству массивов:
r = zeros(len(x))
for i in xrange(len(x)):
r[i] = sin(x[i])*cos(x[i])*exp(-x[i]**2) + 2 + x[i]**2
Это свойство и называется векторизацией. Существенный выигрыш в скорости по сравнению со списками происходит из-за того что в генерации списков используется относительно медленные циклы самого Python, в то время как векторизация их никак явно не использует, а задействует «быстрые циклы» внутри numpy. Кроме того, что векторизация существенно повышает скорость обработки, она делает код более понятным и ясным для чтения.
Но приведенный выше код не является «чистой векторизацией», в нем используется цикл for, без которого можно обойтись, если использовать тригонометрические функции из пакета numpy (которые поддаются векторизации):
from numpy import *
x=linspace(-pi,pi,11)
r = sin(x)*cos(x)*exp(-x**2) + 2 + x**2
Ссылки
[править]- Проект научных вычислений SciPy — отсюда можно скачать пакеты NumPy и SciPy
- Краткое введение в NumPy
- Шпаргалка по массивам в SciPy (NumPy)
- Руководство по NumPy (англ.)