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

Многие специалисты в области научных вычислений довольно предвзято относятся к уже готовым инструментам и математическим функциям в том числе. Однако NumPy, предоставляет, действительно, отличные решения - универсальные функции, на производительность и гибкость которых можно смело положиться в 99% случаев. Универсальная функция - это оболочка над обычной функцией, благодаря которой возможно вполнять определенные действия над целыми массивами. Данные функции выполняют действия над массивами поэлементно, поддерживают механизм транслирования, автоматически преобразуют типы данных и обеспечивают доступ к более тонким настройкам своей работы.

Большинство универсальных функций реализованы в скомпилированном C-коде и могут могут обрабатывать самые разные объекты:

>>> import numpy as np
>>> 
>>> np.sin(60)
-0.3048106211022167
>>> np.sin([0, 30, 60])
array([ 0.        , -0.98803162, -0.30481062])
>>> np.sin((0, 30, 60))
array([ 0.        , -0.98803162, -0.30481062])
>>> np.sin(np.array([0, 30, 60]))
array([ 0.        , -0.98803162, -0.30481062])
>>> np.sin(np.matrix([0, 30, 60]))
matrix([[ 0.        , -0.98803162, -0.30481062]])

Тем не менее есть специализированные универсальные функции, для которых основными аргументами являются матрицы, векторы и т.п.

Необязательные аргументы

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

out

Определяет место в которое будет сохранен результат функции. Если параметр не указан или указано None (по умолчанию), то будет возвращен автоматически созданный массив. Если в качестве параметра указан массив NumPy, то он должен иметь форму совместимую с формой входного массива. Если универсальная функция создает несколько выходных массивов, то для каждого из них можно указать место сохранения с помощью кортежа из массивов NumPy или None.

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

>>> import numpy as np
>>> 
>>> x = np.arange(0, 100, 15)
>>> x
array([ 0, 15, 30, 45, 60, 75, 90])
>>> 
>>> y = np.ones(7)
>>> y
array([1., 1., 1., 1., 1., 1., 1.])
>>> 
>>> np.sin(x, out = y)
array([ 0.        ,  0.65028784, -0.98803162,  0.85090352, -0.30481062,
       -0.38778164,  0.89399666])
>>> 
>>> y
array([ 0.        ,  0.65028784, -0.98803162,  0.85090352, -0.30481062,
       -0.38778164,  0.89399666])
>>> 
>>>
>>> #  В данном параметре можно указывать представления массивов:
...
>>> y = np.zeros(7)
>>> y
array([0., 0., 0., 0., 0., 0., 0.])
>>> 
>>> np.sin(x[::2], out = y[::2])
array([ 0.        , -0.98803162, -0.30481062,  0.89399666])
>>> 
>>> y
array([ 0.        ,  0.        , -0.98803162,  0.        , -0.30481062,
        0.        ,  0.89399666])

Параметр out можно не использовать и просто записывать что-то вроде y[::2] = sin(x[::2]), но следует помнить что такая запись приводит к созданию временного массива для хранения результата вычислений sin(x[::2]) и затем к еще одной операции копирования результатов из временного массива в y[::2]. При обработке больших массивов, использование аргумента out может обеспечить значительный выигрыш в экономии используемой памяти и затраченного на вычисления времени.

where

Данный параметр принимает True (по умолчанию), False или массив логических значений в случае если универсальная функция возвращает несколько результирующих массивов. Значение True указывает на вычисление универсальной функции с сохранением результата в указанный в параметре out массив. В случае указания False, будут возвращены значения массива, который указан в out.

>>> x
array([ 0, 15, 30, 45, 60, 75, 90])
>>> 
>>> np.cos(x, where = False)
array([ 0.        ,  0.65028784, -0.98803162,  0.85090352, -0.30481062,
       -0.38778164,  0.89399666])
>>> 
>>> y = np.zeros(7)
>>> y
array([0., 0., 0., 0., 0., 0., 0.])
>>>
>>> np.cos(x, out = y, where = False)
array([0., 0., 0., 0., 0., 0., 0.])
>>> 
>>> np.cos(x, out = y, where = True)
array([ 1.        , -0.75968791,  0.15425145,  0.52532199, -0.95241298,
        0.92175127, -0.44807362])
>>> 
>>> y
array([ 1.        , -0.75968791,  0.15425145,  0.52532199, -0.95241298,
        0.92175127, -0.44807362])

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

>>> x = np.arange(0, 100, 15)
>>> y = np.zeros(7)
>>> 
>>> a, b = 0, 1     #  Границы интервала
>>> c = 0.77        #  Параметр, определяющий необходимость вычислений
>>> 
>>>  #  Если параметр находится в пределах интервала,
...  #  то результат будет вычислен и помещен в указанный массив:
...
>>> np.cos(x, out = y, where = a < c < b)
array([ 1.        , -0.75968791,  0.15425145,  0.52532199, -0.95241298,
        0.92175127, -0.44807362])
>>> y
array([ 1.        , -0.75968791,  0.15425145,  0.52532199, -0.95241298,
        0.92175127, -0.44807362])
>>> 
>>> x = np.arange(0, 70, 10)     #  Новые данные в массиве 'x'
>>> x
array([ 0, 10, 20, 30, 40, 50, 60])
>>> 
>>>  #  В случае, если бы параметр 'c' оказался в пределах интервала
...  #  от 0 до 1, то в массиве 'y' мы бы увидели:
...
>>> np.cos(x)
array([ 1.        , -0.83907153,  0.40808206,  0.15425145, -0.66693806,
        0.96496603, -0.95241298])
>>> 
>>> c = 1.01    #  Допустим параметр вышел за пределы интервала
>>> 
>>> #  Тогда в массиве 'y' мы увидим предыдущие значения:
>>> np.cos(x, out = y, where = a < c < b)
array([ 1.        , -0.75968791,  0.15425145,  0.52532199, -0.95241298,
        0.92175127, -0.44807362])

casting

Позволяет настроить преобразование типов данных при вычислениях. Данный параметр может принимать следующие значения: 'no' - типы данных не преобразуются; 'equiv' - допускается только изменение порядка байтов; 'safe' - допускаются только те типы данных, которые сохраняют значения; 'same_kind' - допускаются только безопасные преобразования, такие как float64 в float32; 'usafe' - допускаются любые преобразования данных.

>>> x = np.arange(0, 70, 10, dtype = np.float96)
>>> y = np.zeros(7, dtype = np.float16)
>>> 
>>> x
array([ 0., 10., 20., 30., 40., 50., 60.], dtype=float96)
>>> y
array([0., 0., 0., 0., 0., 0., 0.], dtype=float16)
>>> 
>>> np.cos(x)
array([ 1.        , -0.83907153,  0.40808206,  0.15425145, -0.66693806,
        0.96496603, -0.95241298], dtype=float96)
>>> 
>>> np.cos(x, out = y, casting = 'same_kind')    #  значение 'casting' по умолчанию
array([ 1.    , -0.839 ,  0.4082,  0.1543, -0.667 ,  0.965 , -0.9526],
      dtype=float16)
>>> 
>>> y     #  Произошла потеря точности
array([ 1.    , -0.839 ,  0.4082,  0.1543, -0.667 ,  0.965 , -0.9526],
      dtype=float16)
>>> 
>>> np.cos(x, out = y, casting = 'safe')    #  Запрещает небезопасные преобразования
Traceback (most recent call last):
  File "", line 1, in 
TypeError: ufunc 'cos' output (typecode 'g') could not be coerced to provided output
parameter (typecode 'e') according to the casting rule ''safe''

order

Этот параметр определяет в каком порядке выходные массивы должны храниться в памяти: строчном C-стиле или столбчатом стиле Fortran. Если входной массив не является массивом NumPy, то созданный массив будет находиться в памяти в строковом С порядке, если указать флаг 'F', то будет храниться в столбчатом порядке 'Fortran'. Если входной массив - это массив NumPy, то флаг 'K' либо сохраняет порядок исходного массива либо устанавливает самый близкий по структуре; флаг 'A' установит макет памяти выходного массива в 'F' если массив a является смежным со столбчатым стилем Fortran, в противном случае макет памяти будет установлен в 'C'. По умолчанию флаг установлен в значение 'K'.

>>> x = np.arange(0, 100, 30).reshape(2,2)
>>> x
array([[ 0, 30],
       [60, 90]])
>>> y = np.cos(x)
>>> 
>>> y.flags
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  WRITEBACKIFCOPY : False
  UPDATEIFCOPY : False
>>> 
>>> y = np.cos(x, order = 'F')
>>> 
>>> y.flags
  C_CONTIGUOUS : False
  F_CONTIGUOUS : True
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  WRITEBACKIFCOPY : False
  UPDATEIFCOPY : False

dtype

Позволяет переопределить тип данных выходного массива:

>>> x = np.arange(0, 100, 30)
>>> x
array([ 0, 30, 60, 90])
>>> 
>>> y = np.cos(x)
>>> y
array([ 1.        ,  0.15425145, -0.95241298, -0.44807362])
>>> 
>>> y.dtype
dtype('float64')
>>> 
>>> y = np.cos(x, dtype = np.float16)
>>> y
array([ 1.    ,  0.1543, -0.9526, -0.448 ], dtype=float16)

subok

Если установлено значение True, то подклассы будут сохраняться, если False, то результатом будет базовый класс ndarray:

>>> x = np.matrix([0, 30, 60, 90])
>>> x
matrix([[ 0, 30, 60, 90]])
>>> 
>>> y = np.cos(x, subok = True)    #  По умолчанию подклассы сохраняются
>>> y
matrix([[ 1.        ,  0.15425145, -0.95241298, -0.44807362]])
>>> 
>>> y = np.cos(x, subok = False)
>>> y
array([[ 1.        ,  0.15425145, -0.95241298, -0.44807362]])

signature

Большинство универсальных функций реализованы в скомпилированном C-коде. Основные вычисления внутри универсальной функции заложены в ее базовом цикле. Точнее, таких циклов несколько и в зависимости от типа входных данных функция выбирает самый подходящий из них. Аргумент signature позволяет указать какой именно из этих циклов должен использоваться. В качестве аргумента signature нужно указать специальную строку-подпись цикла, либо тип данных, либо кортеж типов данных. Получить список доступных строк-подписей можно с помощью атрибута types объекта ufunc:

>>> import numpy as np
>>>
>>> np.cos.types
['e->e', 'f->f', 'd->d', 'g->g', 'F->F', 'D->D', 'G->G', 'O->O']
>>> 
>>> x = np.arange(0, 100, 30)
>>> x
array([ 0, 30, 60, 90])
>>> 
>>> np.cos(x, signature = 'f->f')
array([ 1.        ,  0.15425146, -0.95241296, -0.44807363], dtype=float32)
>>> 
>>> np.cos(x, signature = 'F->F')
array([ 1.        -0.j,  0.15425146+0.j, -0.95241296+0.j, -0.44807363-0.j],
      dtype=complex64)
>>> 
>>> np.cos(x, signature = 'F')
array([ 1.        -0.j,  0.15425146+0.j, -0.95241296+0.j, -0.44807363-0.j],
      dtype=complex64)
>>> 
>>> np.cos(x, signature = np.complex64)
Traceback (most recent call last):
  File "", line 1, in 
TypeError: No loop matching the specified signature and casting
was found for ufunc cos

Данный параметр предоставляет небольшую, но все же оптимизацию вычислений - вместо автоматического поиска подходящего цикла, функция сразу переходит к вычислениям в указанном вами цикле. Скорее всего указание данного аргумента будет полезным в вычислениях в которых вызов универсальных функций происходит очень часто.

extobj

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

>>> np.cos(x, extobj = [160, 1, 1])
array([ 1.        ,  0.15425145, -0.95241298, -0.44807362])

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