Универсальные функции NumPy

NumPyBeginner
Практиковаться сейчас

Введение

В этой лабораторной работе вы изучите основы универсальных функций (Universal Functions), широко известных как ufuncs, в NumPy. Ufuncs являются краеугольным камнем высокопроизводительных вычислений в Python, позволяя выполнять быстрые поэлементные операции над целыми массивами данных. Мы рассмотрим базовые арифметические операции, мощную концепцию broadcasting (расширения), методы агрегации и способы управления типами данных ваших результатов. К концу этой лабораторной работы вы сможете использовать ufuncs для написания более чистого и эффективного кода обработки данных.

Базовая арифметика с Ufuncs

По своей сути ufuncs выполняют поэлементные операции. Это означает, что когда вы применяете операцию к двум массивам, операция выполняется для каждой пары соответствующих элементов. Наиболее распространенными ufuncs являются стандартные арифметические операторы, такие как +, -, * и /.

Начнем с выполнения простого сложения двух массивов NumPy.

Сначала откройте файл ufunc_examples.py в проводнике файлов слева. Замените существующее содержимое следующим кодом. Этот код импортирует NumPy, создает два массива и складывает их.

import numpy as np

## Создаем два массива
arr1 = np.array([0, 2, 3, 4])
arr2 = np.array([1, 1, -1, 2])

## Оператор '+' является ufunc, который складывает массивы поэлементно
result = arr1 + arr2

## Выводим результат
print("Результат шага 1:")
print(result)

Добавив код, сохраните файл. Теперь запустите скрипт из терминала, чтобы увидеть вывод.

python ufunc_examples.py

Вы должны увидеть результат поэлементного сложения.

Результат шага 1:
[1 3 2 6]

Это демонстрирует фундаментальное поведение ufunc: arr1[0] складывается с arr2[0], arr1[1] с arr2[1] и так далее, создавая новый массив с результатами.

Broadcasting в действии

Broadcasting (расширение) — это мощный механизм, который позволяет NumPy работать с массивами разных форм во время арифметических операций. По сути, NumPy "расширяет" меньший массив до большего, чтобы они имели совместимые формы.

Типичным примером является умножение каждого элемента массива на одно число. Рассмотрим также более сложный случай, когда одномерный массив расширяется до двумерного массива.

Измените ваш файл ufunc_examples.py. Добавьте следующий код в конец скрипта.

## --- Добавленный код для Шага 2 ---

## Расширение скаляра до массива
arr1 = np.array([1, 2, 3])
scalar_result = arr1 * 10
print("\nРезультат шага 2 (Расширение скаляра):")
print(scalar_result)

## Расширение одномерного массива до двумерного массива
arr2d = np.array([[1], [2], [3]]) ## Форма (3, 1)
arr1d = np.array([1, 2, 3])      ## Форма (3,)
broadcast_result = arr2d * arr1d
print("\nРезультат шага 2 (Расширение массива):")
print(broadcast_result)

Сохраните файл и снова запустите его из терминала.

python ufunc_examples.py

Вы увидите вывод как для Шага 1, так и для Шага 2.

Результат шага 1:
[1 3 2 6]

Результат шага 2 (Расширение скаляра):
[10 20 30]

Результат шага 2 (Расширение массива):
[[1 2 3]
 [2 4 6]
 [3 6 9]]

Во втором примере одномерный массив arr1d (форма (3,)) и двумерный массив arr2d (форма (3, 1)) расширяются до общей формы (3, 3) перед выполнением поэлементного умножения.

Агрегация массивов с помощью .reduce()

Помимо поэлементных операций, ufuncs имеют специальные методы для выполнения агрегаций. Метод .reduce() является одним из наиболее полезных. Он многократно применяет ufunc вдоль указанной оси массива, пока не останется только одно измерение.

Например, np.add.reduce(arr) эквивалентно np.sum(arr). Давайте посмотрим, как это работает на двумерном массиве.

Добавьте следующий код в ваш файл ufunc_examples.py.

## --- Добавленный код для Шага 3 ---

## Создаем массив 3x3
arr = np.arange(9).reshape(3, 3)
print("\nИсходный массив Шага 3:")
print(arr)

## Агрегируем массив, суммируя вдоль оси 1 (столбцы)
## Это просуммирует элементы в каждой строке.
## Для строки 0: 0 + 1 + 2 = 3
## Для строки 1: 3 + 4 + 5 = 12
## Для строки 2: 6 + 7 + 8 = 21
reduced_result = np.add.reduce(arr, axis=1)

print("\nРезультат Шага 3 (reduce по оси=1):")
print(reduced_result)

Сохраните файл и выполните его.

python ufunc_examples.py

Вывод теперь будет включать результаты этого шага.

... (предыдущий вывод) ...

Исходный массив Шага 3:
[[0 1 2]
 [3 4 5]
 [6 7 8]]

Результат Шага 3 (reduce по оси=1):
[ 3 12 21]

Как вы видите, .reduce() свернул массив вдоль указанной оси, применяя операцию add к его элементам.

Указание типов данных вывода

Обычно NumPy автоматически определяет тип данных выходного массива. Однако вы можете явно указать тип данных вывода, используя аргумент dtype. Это полезно для контроля использования памяти или обеспечения числовой точности.

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

Добавьте следующий код в конец файла ufunc_examples.py.

## --- Добавленный код для Шага 4 ---

## Используем тот же массив 3x3 из Шага 3
arr = np.arange(1, 10).reshape(3, 3) ## Используем числа от 1 до 9, чтобы избежать умножения на ноль
print("\nИсходный массив Шага 4:")
print(arr)

## Агрегируем с помощью умножения, преобразуя вывод в float
## Для строки 0: 1 * 2 * 3 = 6
## Для строки 1: 4 * 5 * 6 = 120
## Для строки 2: 7 * 8 * 9 = 504
multiply_result = np.multiply.reduce(arr, axis=1, dtype=float)

print("\nРезультат Шага 4 (multiply.reduce с dtype=float):")
print(multiply_result)

Сохраните и запустите скрипт.

python ufunc_examples.py

Наблюдайте за выводом для Шага 4.

... (предыдущий вывод) ...

Исходный массив Шага 4:
[[1 2 3]
 [4 5 6]
 [7 8 9]]

Результат Шага 4 (multiply.reduce с dtype=float):
[  6. 120. 504.]

Обратите внимание на точки после чисел (.) в выходном массиве [ 6. 120. 504.]. Это указывает на то, что элементы теперь являются числами с плавающей запятой, как мы указали с помощью dtype=float.

Переопределение поведения Ufunc

Система ufunc в NumPy расширяема. Вы можете создавать свои собственные объекты, похожие на массивы, которые определяют, как ufuncs должны на них работать. Это продвинутая функция, которая обычно выполняется путем наследования от ndarray NumPy и переопределения специальных методов, таких как __add__ (для оператора +).

Создадим простой пользовательский класс массива, который выводит сообщение при выполнении сложения.

Добавьте этот последний блок кода в ufunc_examples.py.

## --- Добавленный код для Шага 5 ---

## Определяем пользовательский класс массива, наследуя от np.ndarray
class MyArray(np.ndarray):
    def __add__(self, other):
        print("\nШаг 5: Вызван пользовательский метод add!")
        ## Вызываем оригинальную реализацию из родительского класса
        return super().__add__(other)

## Создаем экземпляр нашего пользовательского класса
## Мы должны использовать .view(), чтобы привести ndarray к нашему пользовательскому классу
my_arr = np.array([10, 20, 30]).view(MyArray)

## Выполняем сложение, что вызовет наш пользовательский метод
override_result = my_arr + 5

print("Результат Шага 5 (Переопределенный Ufunc):")
print(override_result)

Сохраните файл и запустите его в последний раз.

python ufunc_examples.py

Проверьте финальный вывод.

... (предыдущий вывод) ...

Шаг 5: Вызван пользовательский метод add!
Результат Шага 5 (Переопределенный Ufunc):
[15 25 35]

Вы можете видеть, что перед результатом сложения было напечатано наше пользовательское сообщение, подтверждая, что наш метод __add__ был вызван. Это демонстрирует мощную гибкость системы ufunc.

Резюме

В этой лабораторной работе вы изучили основы универсальных функций (ufuncs) NumPy. Мы начали с базовых поэлементных арифметических операций, которые составляют основу векторизованных вычислений. Затем вы изучили broadcasting (расширение), ключевую функцию, которая позволяет NumPy выполнять операции над массивами различных форм. Мы также рассмотрели, как использовать методы ufunc, такие как .reduce(), для агрегации данных и как управлять типом данных вывода с помощью аргумента dtype. Наконец, вы увидели продвинутый пример того, как настраивать поведение ufunc путем наследования от np.ndarray. Обладая этими навыками, вы теперь лучше подготовлены к написанию эффективного, читаемого и мощного числового кода с использованием NumPy.