Основы универсальных функций (ufunc)
См. также
Универсальная функция (или ufunc коротко) — это функция, которая работает с ndarrays поэлементно, поддерживая векторное сложение, приведение типов и другие стандартные функции. То есть, ufunc — это «векторизованная» оболочка для функции, принимающей фиксированное количество определённых входных данных и возвращающей фиксированное количество определённых выходных данных.
В NumPy универсальные функции являются экземплярами класса numpy.ufunc. Многие встроенные функции реализованы на скомпилированном С коде. Базовые ufunc работают со скалярами, но также существует обобщённый тип, для которого базовые элементы — подмассивы (векторы, матрицы и т. д.), а векторизация выполняется по другим измерениям. Простейший пример — оператор сложения:
>>> np.array([0,2,3,4]) + np.array([1,1,-1,2]) array([1, 3, 2, 6])
Также можно создавать пользовательские экземпляры numpy.ufunc с помощью фабричной функции numpy.frompyfunc.
Методы ufunc
Все ufunc имеют четыре метода. Их можно найти в разделе Методы. Однако эти методы имеют смысл только для скалярных ufunc, принимающих два входных аргумента и возвращающих один выходной аргумент. Попытка вызвать эти методы для других ufunc вызовет исключение ValueError.
Методы, похожие на reduce, принимают ключевое слово axis, ключевое слово dtype и ключевое слово out, и массивы должны иметь размерность >= 1. Ключевое слово axis указывает ось массива, по которой будет выполняться сокращение (отрицательные значения отсчитываются назад). Обычно это целое число, хотя для numpy.ufunc.reduce оно также может быть кортежем int для сокращения по нескольким осям сразу или None, чтобы выполнить сокращение по всем осям. Например:
>>> x = np.arange(9).reshape(3,3)
>>> x
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
>>> np.add.reduce(x, 1)
array([ 3, 12, 21])
>>> np.add.reduce(x, (0, 1))
36
Ключевое слово dtype позволяет управлять очень распространённой проблемой, возникающей при небрежном использовании ufunc.reduce. Иногда у вас может быть массив определённого типа данных, и вы хотите сложить все его элементы, но результат не помещается в тип данных массива. Это часто происходит, если у вас есть массив целых чисел с одним байтом. Ключевое слово dtype позволяет изменить тип данных, по которому происходит сокращение (и, следовательно, тип выходных данных). Таким образом, вы можете гарантировать, что выходные данные имеют тип с достаточной точностью для обработки результата. Ответственность за изменение типа сокращения в основном лежит на вас. Есть одно исключение: если для сокращения по операциям «сложение» или «умножение» не указан параметр dtype, то если входной тип — целые числа (или булевы значения) и меньше размера типа данных numpy.int_, он будет автоматически преобразован к типу int_ (или numpy.uint). В предыдущем примере:
>>> x.dtype
dtype('int64')
>>> np.multiply.reduce(x, dtype=float)
array([ 0., 28., 80.])
Наконец, ключевое слово out позволяет предоставить выходной массив (или кортеж выходных массивов для ufunc с несколькими выходными значениями). Если out задан, аргумент dtype используется только для внутренних вычислений. Рассматривая x из предыдущего примера:
>>> y = np.zeros(3, dtype=int) >>> y array([0, 0, 0]) >>> np.multiply.reduce(x, dtype=float, out=y) array([ 0, 28, 80])
Ufunc также имеет пятый метод, numpy.ufunc.at, который позволяет выполнять операции на месте с помощью расширенных индексов. Буферизация не используется на измерениях, где применяются расширенные индексы, поэтому расширенный индекс может указывать на один и тот же элемент несколько раз, и операция будет выполняться над результатом предыдущей операции для этого элемента.
Определение типа выходных данных
Выходные данные ufunc (и его методов) не обязательно являются ndarray, если все входные аргументы не являются ndarrays. Действительно, если любой входной аргумент определяет метод __array_ufunc__, управление полностью передаётся этой функции, т. е. ufunc переопределяется.
Если ни один из входов не переопределяет ufunc, все выходные массивы будут переданы методу __array_wrap__ входного аргумента (кроме ndarrays и скаляров), который его определяет и имеет наивысший приоритет __array_priority__ по сравнению с другими входными данными универсальной функции. По умолчанию приоритет __array_priority__ массива ndarray равен 0,0, и по умолчанию приоритет __array_priority__ подтипа равен 0,0. Матрицы имеют приоритет __array_priority__, равный 10,0.
Все ufunc также могут принимать выходные аргументы, которые должны быть массивами или подклассами. При необходимости результат будет приведён к типу данных(ам) предоставленного(ых) выходного(ых) массива(ов). Если выходной массив имеет метод __array_wrap__, он будет вызван вместо метода, найденного в входных данных.
Векторное сложение
См. также
Каждая универсальная функция принимает входные массивы и производит выходные массивы, выполняя основную функцию поэлементно над входами (где элемент, как правило, скаляр, но может быть вектором или подмассивом более высокого порядка для обобщённых ufunc). Применяются стандартные правила векторизации, поэтому входы, не имеющие точно одинаковых форм, всё ещё могут быть успешно обработаны.
Согласно этим правилам, если у входного массива размерность измерения равна 1 в его форме, первый элемент данных в этом измерении будет использоваться для всех вычислений по этому измерению. Другими словами, механизм шага ufunc просто не будет переходить по этому измерению (шаг stride для этого измерения будет равен 0).
Правила приведения типов
Примечание
В NumPy 1.6.0 был создан API для приведения типов, чтобы инкапсулировать механизм определения типов вывода. См. функции numpy.result_type, numpy.promote_types и numpy.min_scalar_type для получения более подробной информации.
В основе каждой ufunc лежит одномерный цикл с шагом, реализующий фактическую функцию для определенной комбинации типов. При создании ufunc ей предоставляется статический список внутренних циклов и соответствующий список сигнатур типов, по которым ufunc работает. Механизм ufunc использует этот список, чтобы определить, какой внутренний цикл использовать для конкретного случая. Вы можете проверить атрибут .types для конкретной ufunc, чтобы увидеть, какие комбинации типов имеют определенный внутренний цикл и какой тип вывода они производят (коды символов используются в указанном выводе для краткости).
Приведение типов должно быть выполнено для одного или нескольких входных данных всякий раз, когда ufunc не имеет реализации основного цикла для предоставленных входных типов. Если реализация для входных типов не найдена, то алгоритм ищет реализацию с сигнатурой типа, к которой все входные данные могут быть приведены «безопасно». Первый найденный в своем внутреннем списке циклов выбирается и выполняется после всех необходимых приведений типов. Следует помнить, что внутренние копии во время ufunc (даже для приведения типов) ограничены размером внутреннего буфера (который можно настроить пользователем).
Примечание
Универсальные функции в NumPy достаточно гибкие, чтобы иметь смешанные сигнатуры типов. Таким образом, например, можно определить универсальную функцию, которая работает с плавающей точкой и целыми числами. См. numpy.ldexp для примера.
Согласно вышесказанному, правила приведения типов по существу реализуются вопросом о том, когда тип данных может быть приведен «безопасно» к другому типу данных. Ответ на этот вопрос можно определить в Python с помощью вызова функции: can_cast(fromtype, totype). Приведенный ниже пример показывает результаты этого вызова для 24 поддерживаемых внутри типов на 64-битной системе автора. Вы можете сгенерировать эту таблицу для своей системы с помощью кода, приведенного в примере.
Пример
Фрагмент кода, показывающий таблицу «безопасное приведение» для 64-битной системы. В общем случае вывод зависит от системы; ваша система может привести к другой таблице.
>>> mark = {False: ' -', True: ' Y'}
>>> def print_table(ntypes):
... print('X ' + ' '.join(ntypes))
... for row in ntypes:
... print(row, end='')
... for col in ntypes:
... print(mark[np.can_cast(row, col)], end='')
... print()
...
>>> print_table(np.typecodes['All'])
X ? b h i l q n p B H I L Q N P e f d g F D G S U V O M m
? Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y - Y
b - Y Y Y Y Y Y Y - - - - - - - Y Y Y Y Y Y Y Y Y Y Y - Y
h - - Y Y Y Y Y Y - - - - - - - - Y Y Y Y Y Y Y Y Y Y - Y
i - - - Y Y Y Y Y - - - - - - - - - Y Y - Y Y Y Y Y Y - Y
l - - - - Y Y Y Y - - - - - - - - - Y Y - Y Y Y Y Y Y - Y
q - - - - Y Y Y Y - - - - - - - - - Y Y - Y Y Y Y Y Y - Y
n - - - - Y Y Y Y - - - - - - - - - Y Y - Y Y Y Y Y Y - Y
p - - - - Y Y Y Y - - - - - - - - - Y Y - Y Y Y Y Y Y - Y
B - - Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y - Y
H - - - Y Y Y Y Y - Y Y Y Y Y Y - Y Y Y Y Y Y Y Y Y Y - Y
I - - - - Y Y Y Y - - Y Y Y Y Y - - Y Y - Y Y Y Y Y Y - Y
L - - - - - - - - - - - Y Y Y Y - - Y Y - Y Y Y Y Y Y - -
Q - - - - - - - - - - - Y Y Y Y - - Y Y - Y Y Y Y Y Y - -
N - - - - - - - - - - - Y Y Y Y - - Y Y - Y Y Y Y Y Y - -
P - - - - - - - - - - - Y Y Y Y - - Y Y - Y Y Y Y Y Y - -
e - - - - - - - - - - - - - - - Y Y Y Y Y Y Y Y Y Y Y - -
f - - - - - - - - - - - - - - - - Y Y Y Y Y Y Y Y Y Y - -
d - - - - - - - - - - - - - - - - - Y Y - Y Y Y Y Y Y - -
g - - - - - - - - - - - - - - - - - - Y - - Y Y Y Y Y - -
F - - - - - - - - - - - - - - - - - - - Y Y Y Y Y Y Y - -
D - - - - - - - - - - - - - - - - - - - - Y Y Y Y Y Y - -
G - - - - - - - - - - - - - - - - - - - - - Y Y Y Y Y - -
S - - - - - - - - - - - - - - - - - - - - - - Y Y Y Y - -
U - - - - - - - - - - - - - - - - - - - - - - - Y Y Y - -
V - - - - - - - - - - - - - - - - - - - - - - - - Y Y - -
O - - - - - - - - - - - - - - - - - - - - - - - - - Y - -
M - - - - - - - - - - - - - - - - - - - - - - - - Y Y Y -
m - - - - - - - - - - - - - - - - - - - - - - - - Y Y - Y
Следует отметить, что типы «S», «U» и «V», хотя и включены в таблицу для полноты, не могут обрабатываться ufunc. Также обратите внимание, что на 32-битной системе целые типы могут иметь разные размеры, что приводит к несколько измененной таблице.
Операции со смешанными скалярами и массивами используют другой набор правил приведения типов, которые гарантируют, что скаляр не может «приводить к расширению» массив, если скаляр не является фундаментально другого рода данных (т. е. находится в другой иерархии в иерархии типов данных), чем массив. Это правило позволяет использовать скалярные константы в вашем коде (которые в качестве типов Python интерпретируются соответствующим образом в ufunc), не беспокоясь о том, что точность скалярной константы вызовет приведение к расширению вашего большого массива (малой точности).
Использование внутренних буферов
Внутренне буферы используются для невыровненных данных, данных, требующих перестановки, и данных, которые должны быть преобразованы из одного типа данных в другой. Размер внутренних буферов настраивается для каждой нити. Может быть создано до \(2 (n_{\mathrm{inputs}} + n_{\mathrm{outputs}})\) буферов указанного размера для обработки данных со всех входных и выходных данных ufunc. По умолчанию размер буфера составляет 10 000 элементов. Всякий раз, когда потребуется вычисление с использованием буферов, но все входные массивы меньше размера буфера, эти некорректные или неверно типизированные массивы будут скопированы перед выполнением вычислений. Таким образом, изменение размера буфера может повлиять на скорость выполнения вычислений ufunc различных типов.
Простой интерфейс для установки этой переменной доступен с помощью функции numpy.setbufsize.
Обработка ошибок
Универсальные функции могут вызывать специальные регистры статуса чисел с плавающей точкой в вашем оборудовании (такие как деление на ноль). Если они доступны на вашей платформе, эти регистры будут регулярно проверяться во время вычислений. Обработка ошибок контролируется по каждой нити и может быть настроена с помощью функций numpy.seterr и numpy.seterrcall.
Переопределение поведения ufunc
Классы (включая подклассы ndarray) могут переопределить, как ufunc действуют на них, определяя определенные специальные методы. Подробности см. в Стандартных подклассах массивов.
© 2005–2024 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/2.0/user/basics.ufuncs.html