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

Универсальная функция (ufunc) — это функция NumPy, которая применяется к массивам поэлементно, поддерживает broadcasting и работает на C-скорости. Примеры: np.add, np.sqrt, np.sin, np.maximum. Когда вы пишете a + b для двух массивов, под капотом вызывается np.add — это и есть ufunc.

Все ufunc делятся на унарные (один аргумент: np.sqrt, np.exp) и бинарные (два аргумента: np.add, np.multiply). У каждой бинарной ufunc есть полезные методы: reduce, accumulate, outer, reduceat.

Зачем нужно

  • Скорость. Цикл for i in range(...) в Python в 50–100 раз медленнее, чем ufunc на тех же данных.

  • Краткость. np.sin(angles) понятнее, чем цикл с math.sin.

  • Broadcasting. ufunc автоматически согласует формы массивов разной размерности.

  • Универсальный интерфейс. Любая ufunc работает с любым dtype и любой формой.

Пример 1. Поэлементная арифметика

import numpy as np

a = np.array([1, 2, 3, 4])
b = np.array([10, 20, 30, 40])

print(np.add(a, b))        # то же, что a + b
print(np.multiply(a, b))   # то же, что a * b
print(np.power(a, 2))      # квадрат каждого
[11 22 33 44]
[ 10  40  90 160]
[ 1  4  9 16]

Пример 2. Математические функции

import numpy as np

angles_deg = np.array([0, 30, 45, 60, 90])
angles_rad = np.deg2rad(angles_deg)

print(np.round(np.sin(angles_rad), 3))
print(np.round(np.cos(angles_rad), 3))
print(np.round(np.exp(np.array([0, 1, 2])), 3))
[0.    0.5   0.707 0.866 1.   ]
[1.    0.866 0.707 0.5   0.   ]
[1.    2.718 7.389]

Пример 3. Broadcasting

ufunc автоматически «растягивают» массивы меньшей размерности.

import numpy as np

# Матрица 3x4 — например, 3 датчика по 4 измерения
data = np.array([
    [20, 21, 19, 22],
    [25, 26, 24, 27],
    [30, 31, 29, 32],
])
offsets = np.array([0.5, -0.3, 0.1, 0.0])  # калибровка по столбцам

calibrated = data + offsets   # offsets «растягивается» на каждую строку
print(calibrated)
[[20.5 20.7 19.1 22. ]
 [25.5 25.7 24.1 27. ]
 [30.5 30.7 29.1 32. ]]

Пример 4. Метод reduce

ufunc.reduce сворачивает массив по оси, последовательно применяя ufunc.

import numpy as np

a = np.array([1, 2, 3, 4, 5])
print(np.add.reduce(a))       # сумма = ((((1+2)+3)+4)+5)
print(np.multiply.reduce(a))  # факториал 5!
print(np.maximum.reduce(a))   # максимум
15
120
5

Для матриц можно указать ось:

import numpy as np

m = np.array([[1, 2, 3], [4, 5, 6]])
print(np.add.reduce(m, axis=0))  # суммы по столбцам
print(np.add.reduce(m, axis=1))  # суммы по строкам
[5 7 9]
[ 6 15]

Пример 5. Метод accumulate

accumulate — как reduce, но сохраняет промежуточные результаты. Удобно для скользящих сумм.

import numpy as np

pulses = np.array([1, 0, 1, 1, 0, 1, 1, 1])
total = np.add.accumulate(pulses)
print(total)   # счётчик импульсов
[1 1 2 3 3 4 5 6]

Эквивалент функции np.cumsum, но через метод ufunc это можно делать с любой бинарной операцией — например, np.maximum.accumulate для бегущего максимума.

Пример 6. Поэлементные сравнения и логика

Операторы сравнения тоже ufunc.

import numpy as np

temps = np.array([18.0, 22.5, 25.1, 30.2, 19.7])
hot = np.greater(temps, 24)        # то же, что temps > 24
safe = np.less_equal(temps, 30)    # то же, что temps <= 30
print(hot)
print(safe)
print(np.logical_and(hot, safe))
[False False  True  True False]
[ True  True  True False  True]
[False False  True False False]

Пример 7. where — условие в виде ufunc

np.where(cond, a, b) возвращает a, где cond истинно, и b иначе — поэлементно.

import numpy as np

raw = np.array([-3.0, 1.5, -0.1, 4.2, -5.0])
# ограничим отрицательные нулём (как ReLU)
clipped = np.where(raw < 0, 0, raw)
print(clipped)
[0.  1.5 0.  4.2 0. ]

Пример 8. Своя ufunc через np.frompyfunc

Если нужной операции нет в NumPy, можно «обернуть» Python-функцию. Скорость будет ниже встроенных ufunc, но синтаксис тот же.

import numpy as np

def adc_to_voltage(adc):
    # 10-битный АЦП Arduino, 5В опорное
    return adc * (5.0 / 1023.0)

uf = np.frompyfunc(adc_to_voltage, 1, 1)
readings = np.array([0, 256, 512, 768, 1023])
volts = uf(readings).astype(float)
print(np.round(volts, 3))
[0.    1.251 2.503 3.754 5.   ]

Для производительной собственной ufunc лучше использовать numba или cython.

Подводные камни

  • dtype overflow. np.array([200], dtype=np.uint8) + 100 даёт [44] из-за переполнения. NumPy не предупреждает — следите за типами.

  • Несовместимые формы. Broadcasting работает только если размерности либо равны, либо одна из них равна 1. Иначе — ValueError.

  • Деление на ноль. np.divide(1, 0) вернёт inf и предупреждение, а не исключение. Контролируйте через np.errstate.

  • frompyfunc возвращает object. Результат имеет dtype=object — почти всегда нужно явно привести к float/int.

  • reduce на пустом массиве. Без initial упадёт с ошибкой для большинства ufunc.

См. также

Примечание

Лицензия и источники

Техническое описание адаптировано из официальной документации NumPy (https://numpy.org/doc/stable/), доступной под BSD-3-Clause License. Перевод на русский, примеры и пояснения — © AlashEd Wiki.