How to Create Custom NumPy Ufuncs in Python
Universal functions (ufuncs) in NumPy operate element-by-element on
ndarray objects, providing native support for broadcasting,
type casting, and methods like reduction and accumulation. This guide
outlines the standard procedures for authoring custom vectorized ufuncs,
spanning pure Python prototyping with numpy.frompyfunc,
high-performance JIT compilation via Numba, and native extension
development using the NumPy C API.
Method 1: Rapid
Prototyping with numpy.frompyfunc
The standard library method for turning a standard Python function
into a ufunc is numpy.frompyfunc. It wraps an arbitrary
Python callable so that it supports NumPy broadcasting and array
manipulation.
Step-by-Step Procedure
- Define a standard scalar function: Write a function that accepts individual elements as arguments and returns the computed result.
- Apply
numpy.frompyfunc: Specify the function, the number of input arguments (nin), and the number of returned outputs (nout).
import numpy as np
# 1. Define scalar operation
def custom_add_subtract(x, y):
return x + y, x - y
# 2. Convert to ufunc: 2 inputs, 2 outputs
my_ufunc = np.frompyfunc(custom_add_subtract, 2, 2)
# Usage with broadcasting
a = np.array([10, 20, 30])
b = np.array([1, 2, 3])
sum_res, diff_res = my_ufunc(a, b)
print(sum_res) # array([11, 22, 33], dtype=object)
print(diff_res) # array([9, 18, 27], dtype=object)Note: numpy.frompyfunc always returns arrays of
object dtype. If you require typed outputs, use
numpy.vectorize with the otypes parameter,
though neither approach bypasses Python interpreter overhead.
Method 2: High-Performance Ufuncs with Numba
To achieve C-like execution speeds without writing C extensions, use
Numba’s @vectorize decorator. This compiles the Python code
into machine code and creates a genuine NumPy ufunc that avoids Python
GIL overhead.
Step-by-Step Procedure
- Define the scalar operation.
- Apply the
@vectorizedecorator: Pass the desired type signatures (e.g.,'float64(float64, float64)') and target architecture.
from numba import vectorize, float64
import numpy as np
# Define signature: return_type(input1_type, input2_type)
@vectorize([float64(float64, float64)], target='parallel')
def fast_power_diff(x, y):
return (x ** 2) - (y ** 2)
a = np.linspace(0, 10, 1000000)
b = np.linspace(10, 20, 1000000)
result = fast_power_diff(a, b)Functions created with Numba inherit full ufunc features, including
methods such as .reduce():
# Sum-reduction over the array using the custom ufunc
total = fast_power_diff.reduce(a)Method 3: Native C API Implementation
For absolute control and direct inclusion into compiled extensions,
define the ufunc using NumPy's C API with
PyUFunc_FromFuncAndData.
Step-by-Step Procedure
- Write the 1D loop function: Define a function
matching the signature
void loop(char **args, const npy_intp *dimensions, const npy_intp *steps, void *data). - Define type arrays: Declare an array of
charspecifying input and output types using NumPy type characters (such asNPY_DOUBLE). - Register the ufunc: Call
PyUFunc_FromFuncAndDatain the module initialization code.
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
#include <Python.h>
#include <numpy/arrayobject.h>
#include <numpy/ufuncobject.h>
#include <math.h>
/* 1. The 1D loop */
static void double_logit(char **args, const npy_intp *dimensions,
const npy_intp *steps, void *data) {
npy_intp n = dimensions[0];
char *in = args[0];
char *out = args[1];
npy_intp in_step = steps[0];
npy_intp out_step = steps[1];
for (npy_intp i = 0; i < n; i++) {
double val = *(double *)in;
*(double *)out = log(val / (1.0 - val));
in += in_step;
out += out_step;
}
}
/* 2. Type information */
static PyUFuncGenericFunction funcs[1] = {&double_logit};
static char types[2] = {NPY_DOUBLE, NPY_DOUBLE};
static void *data[1] = {NULL};
/* 3. Module Initialization */
static PyMethodDef LogitMethods[] = {
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"custom_ufuncs",
NULL,
-1,
LogitMethods
};
PyMODINIT_FUNC PyInit_custom_ufuncs(void) {
PyObject *m = PyModule_Create(&moduledef);
if (!m) return NULL;
import_array();
import_ufunc();
PyObject *logit_ufunc = PyUFunc_FromFuncAndData(
funcs, data, types,
1, /* number of supported type variants */
1, /* number of inputs */
1, /* number of outputs */
PyUFunc_None, "logit", "Computes the logit function", 0
);
PyModule_AddObject(m, "logit", logit_ufunc);
return m;
}Compile this file using setuptools with
numpy.get_include() added to the extension's include
directories to import and use the compiled ufunc in Python.