Skip to content

Universal Functions

Universal functions (ufuncs) operate element-wise on arrays with broadcasting support.

What are Ufuncs

1. Definition

A ufunc operates on ndarrays element-by-element, supporting broadcasting and type casting.

import numpy as np

def main():
    a = np.array([1, 2, 3])
    b = np.array([4, 5, 6])

    # np.add is a ufunc
    c = np.add(a, b)

    print(f"a = {a}")
    print(f"b = {b}")
    print(f"np.add(a, b) = {c}")
    print(f"Type: {type(np.add)}")

if __name__ == "__main__":
    main()

2. Without Ufuncs

import numpy as np

def main():
    a = [1, 2, 3]
    b = [4, 5, 6]

    # Without ufuncs: manual loop
    c = []
    for i, j in zip(a, b):
        c.append(i + j)

    print(f"Result: {c}")

if __name__ == "__main__":
    main()

3. With Ufuncs

import numpy as np

def main():
    a = np.array([1, 2, 3])
    b = np.array([4, 5, 6])

    # With ufuncs: single expression
    c = a + b

    print(f"Result: {c}")

if __name__ == "__main__":
    main()

Built-in Ufuncs

1. Math Ufuncs

import numpy as np

def main():
    x = np.array([1, 4, 9, 16])

    print(f"x = {x}")
    print(f"np.sqrt(x) = {np.sqrt(x)}")
    print(f"np.square(x) = {np.square(x)}")
    print(f"np.abs(x) = {np.abs(x)}")

if __name__ == "__main__":
    main()

2. Trigonometric Ufuncs

import numpy as np

def main():
    x = np.array([0, np.pi/6, np.pi/4, np.pi/3, np.pi/2])

    print(f"x (radians) = {x}")
    print(f"np.sin(x) = {np.sin(x).round(4)}")
    print(f"np.cos(x) = {np.cos(x).round(4)}")

if __name__ == "__main__":
    main()

3. Comparison Ufuncs

import numpy as np

def main():
    a = np.array([1, 2, 3])
    b = np.array([3, 2, 1])

    print(f"np.greater(a, b) = {np.greater(a, b)}")
    print(f"np.maximum(a, b) = {np.maximum(a, b)}")
    print(f"np.minimum(a, b) = {np.minimum(a, b)}")

if __name__ == "__main__":
    main()

np.frompyfunc

1. Create Custom Ufunc

import numpy as np

def main():
    # Python function
    def my_func(x):
        return x ** 2 + 1

    # Convert to ufunc
    my_ufunc = np.frompyfunc(my_func, 1, 1)

    a = np.array([1, 2, 3, 4])
    result = my_ufunc(a)

    print(f"a = {a}")
    print(f"my_ufunc(a) = {result}")
    print(f"Result dtype: {result.dtype}")

if __name__ == "__main__":
    main()

2. Signature

import numpy as np

def main():
    # np.frompyfunc(func, nin, nout)
    # nin: number of input arguments
    # nout: number of output values

    # Example: function with 2 inputs, 1 output
    def combine(x, y):
        return x * 10 + y

    combine_ufunc = np.frompyfunc(combine, 2, 1)

    a = np.array([1, 2, 3])
    b = np.array([4, 5, 6])

    result = combine_ufunc(a, b)
    print(f"Result: {result}")

if __name__ == "__main__":
    main()

3. String Conversion

import numpy as np

def main():
    a = np.array([1, 2, 3])

    # str() on array gives string representation of whole array
    print(f"str(a) = {str(a)}")

    # frompyfunc applies str to each element
    str_ufunc = np.frompyfunc(str, 1, 1)
    print(f"str_ufunc(a) = {str_ufunc(a)}")

if __name__ == "__main__":
    main()

np.vectorize

1. Basic Usage

import numpy as np

def main():
    def my_func(x):
        if x < 0:
            return 0
        elif x > 10:
            return 10
        else:
            return x

    # Vectorize the function
    vec_func = np.vectorize(my_func)

    a = np.array([-5, 3, 8, 15, 2])
    result = vec_func(a)

    print(f"a = {a}")
    print(f"vec_func(a) = {result}")

if __name__ == "__main__":
    main()

2. Specify Output Type

import numpy as np

def main():
    def classify(x):
        if x < 0:
            return "negative"
        elif x == 0:
            return "zero"
        else:
            return "positive"

    vec_classify = np.vectorize(classify, otypes=[object])

    a = np.array([-2, 0, 3])
    result = vec_classify(a)

    print(f"a = {a}")
    print(f"Classification: {result}")

if __name__ == "__main__":
    main()

3. Excluded Arguments

import numpy as np

def main():
    def power_with_offset(x, n, offset):
        return x ** n + offset

    # Exclude 'offset' from vectorization
    vec_func = np.vectorize(power_with_offset, excluded=['offset'])

    a = np.array([1, 2, 3, 4])
    result = vec_func(a, 2, offset=10)

    print(f"a = {a}")
    print(f"Result: {result}")

if __name__ == "__main__":
    main()

Performance Note

1. vectorize is Not Fast

import numpy as np
import time

def main():
    def square(x):
        return x ** 2

    vec_square = np.vectorize(square)

    a = np.random.randn(1_000_000)

    # Vectorized (slow)
    start = time.perf_counter()
    result1 = vec_square(a)
    vec_time = time.perf_counter() - start

    # Native ufunc (fast)
    start = time.perf_counter()
    result2 = np.square(a)
    native_time = time.perf_counter() - start

    print(f"np.vectorize: {vec_time:.4f} sec")
    print(f"np.square:    {native_time:.6f} sec")
    print(f"Speedup:      {vec_time/native_time:.0f}x")

if __name__ == "__main__":
    main()

2. When to Use

  • np.vectorize: Convenience, not performance
  • Use native ufuncs when available
  • For performance: use NumPy operations or Numba

3. Documentation Quote

The vectorize function is provided primarily for convenience, not for performance. The implementation is essentially a for loop.

Ufunc Attributes

1. nin and nout

import numpy as np

def main():
    print(f"np.add.nin = {np.add.nin}")    # 2 inputs
    print(f"np.add.nout = {np.add.nout}")  # 1 output
    print()
    print(f"np.sqrt.nin = {np.sqrt.nin}")  # 1 input
    print(f"np.sqrt.nout = {np.sqrt.nout}")  # 1 output
    print()
    print(f"np.divmod.nin = {np.divmod.nin}")  # 2 inputs
    print(f"np.divmod.nout = {np.divmod.nout}")  # 2 outputs

if __name__ == "__main__":
    main()

2. ntypes

import numpy as np

def main():
    print(f"np.add.ntypes = {np.add.ntypes}")
    print()
    print("Supported type signatures:")
    for sig in np.add.types[:5]:
        print(f"  {sig}")
    print("  ...")

if __name__ == "__main__":
    main()

Ufunc Methods

1. reduce

import numpy as np

def main():
    a = np.array([1, 2, 3, 4, 5])

    # Reduce applies operation cumulatively
    result = np.add.reduce(a)  # Same as np.sum

    print(f"a = {a}")
    print(f"np.add.reduce(a) = {result}")
    print(f"np.sum(a) = {np.sum(a)}")

if __name__ == "__main__":
    main()

2. accumulate

import numpy as np

def main():
    a = np.array([1, 2, 3, 4, 5])

    # Accumulate keeps intermediate results
    result = np.add.accumulate(a)  # Same as np.cumsum

    print(f"a = {a}")
    print(f"np.add.accumulate(a) = {result}")
    print(f"np.cumsum(a) = {np.cumsum(a)}")

if __name__ == "__main__":
    main()

3. outer

import numpy as np

def main():
    a = np.array([1, 2, 3])
    b = np.array([10, 20])

    # Outer applies operation to all pairs
    result = np.multiply.outer(a, b)

    print(f"a = {a}")
    print(f"b = {b}")
    print("np.multiply.outer(a, b) =")
    print(result)

if __name__ == "__main__":
    main()

4. at

import numpy as np

def main():
    a = np.array([1, 2, 3, 4, 5])
    indices = np.array([0, 2, 2])  # Note: index 2 appears twice

    # Add 10 at specified indices (in-place)
    np.add.at(a, indices, 10)

    print(f"Result: {a}")
    # Index 2 was incremented twice!

if __name__ == "__main__":
    main()

Common Ufuncs

1. Math Operations

Ufunc Description
np.add Addition
np.subtract Subtraction
np.multiply Multiplication
np.divide Division
np.power Power
np.sqrt Square root
np.abs Absolute value

2. Trigonometric

Ufunc Description
np.sin Sine
np.cos Cosine
np.tan Tangent
np.arcsin Inverse sine
np.arctan2 Two-argument arctangent

3. Comparison

Ufunc Description
np.greater Greater than
np.less Less than
np.equal Equal
np.maximum Element-wise maximum
np.minimum Element-wise minimum