Базовые математические операции над массивами

Теперь, когда мы знаем как формировать массивы пришло время самого главного, самого важно для чего, собственно, и был разработан пакет NumPy – к рассмотрению его математических, вычислительных возможностей. И в основе своей эти вычисления базируются на матрицах и векторах. Но, обо всем по порядку и вначале поговорим о базовых математических операциях этого пакета.

Предположим, имеется обычный список и массив, созданный на его основе:

lst = [1, 2, 3]
a = np.array([1, 2, 3])

Сейчас вы увидите принципиальную разницу между двумя этими объектами. Умножим их на число 2:

lst*2 # список: [1, 2, 3, 1, 2, 3]
a*2 # массив: array([2, 4, 6])

Видите, при умножении списка языка Python, он дублируется дважды, а при умножении на NumPy массив – каждый его элемент умножается математически на число 2. Если бы мы захотели то же самое реализовать непосредственно на Python, оперируя списками, то пришлось бы делать что-то вроде:

[x*2 for x in lst]

Но выполнялась бы такая конструкция дольше, чем в случае с массивом NumPy. Именно благодаря удобству и скорости библиотека NumPy и приобрела свою популярность.

Итак, с массивами NumPy мы можем выполнять все базовые математические операции:

Операция

Описание

+

Сложение массивов или массива с числом

-

Вычитание массивов или массива с числом, либо используется как унарный минус

*

Умножение массивов или массива с числом

/

Деление массивов или массива с числом

//

Целочисленное деление массивов или массива с числом

**

Возведение в степень (указывается или число или массив)

%

Вычисления остатка от деления (указывается или число или массив)

Пусть у нас задан тот же одномерный массив:

a = np.array([1, 2, 3])

Все указанные в таблице операции выполняются следующим образом:

-a # унарный минус
a + 2 # сложение с числом
2 + a  # так тоже можно записывать
a - 3 # вычитание с числом
a * 5 # умножение на число
a / 5  # деление на число
a // 2  # целочисленное деление
a ** 3  # возведение в степень 3
a % 2  # вычисление по модулю 2

Разумеется, приоритеты этих операций такие же, что и при обычных математических вычислениях. А на выходе мы получаем новый массив с соответствующими значениями.

Давайте теперь добавим еще один массив:

b = np.array([3, 4, 5])

и посмотрим на эти же операции, но с участием двух массивов:

a - b  # array([-2, -2, -2])
b + a  # array([4, 6, 8])
a * b  # array([ 3,  8, 15])
b / a  # array([3. , 2. , 1.66666667])
b // a # array([3, 2, 1], dtype=int32)
b ** a # array([  3,  16, 125], dtype=int32)
b % a  # array([0, 0, 2], dtype=int32)

Везде мы видим поэлементные операции. Соответственно, чтобы они выполнялись, массивы должны быть согласованы по длине. Например, если взять массив:

b = np.array([3, 4, 5, 6])

и выполнить операцию:

a + b  # ошибка: длины массивов не совпадают

то возникнет ошибка из-за несовпадения длин массивов. Но вот такая операция с двумерным массивом b сработает:

b = np.arange(1, 7)
b.resize(2, 3)
a + b

В этом случае массив a будет применен к каждой строке массива b и на выходе увидим результат:

Такое поведение получило название транслирование массивов. Подробнее о нем мы еще будем говорить.

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

a = np.arange(1, 19)
a.resize(3, 3, 2)
b = np.ones((3, 2))

С ними можно выполнять такие очевидные операции:

a - b
a * 10
a // b

И так далее. Причем, двумерный массив b может быть применен к трехмерному массиву a благодаря операции транслирования, так как их размеры согласованы. На самом деле, умножение трехмерного массива на число также реализуется через транслирование числа по всем элементам массива a.

Все представленные математические операции имеют следующие расширенные аналоги:

Операция

Расшифровка

a += b

a = a + b

a -= b

a = a - b

a * b

a = a * b

a / b

a = a / b

a // b

a = a // b

a ** b

a = a ** b

a % b

a = a % b

То есть, если нам нужно произвести какие-либо математические операции с массивом и изменения сохранить в нем же, то удобно использовать такие сокращенные записи. Выполняются они очевидным образом, например, так:

a = np.array([1, 2, 6, 8])
a += 5
b = np.ones(4)
b *= a

И так далее. Но есть один нюанс работы этих операторов. Если, например, массив b определить с типом данных float64:

b = np.ones(4, dtype='float64')

а массив a имеет тип int32 (можно посмотреть через свойство a.dtype), то операция:

a += b

приведет к ошибке. С чем это связано? Дело в том, что результатом сложения вещественного числа с целочисленным, итоговое значение представляет собой вещественное число. Но тип данных массива a – целочисленный и он не может сохранять вещественные числа. Отсюда и возникает эта ошибка. Вообще, следует помнить правило:

При выполнении арифметических операций тип данных автоматически приводится к более общему.

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

Все описанные математические операции можно комбинировать и записывать в виде:

(a + b)*5 - 10

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

Вот так работают и используются базовые математические операции в пакете NumPy при работе с массивами.