NumPy: ufunc 深入

难度:⭐⭐⭐ | 关键词ufunc, out, reduce, accumulate, outer, frompyfunc, vectorize

Alice 发现 np.add(a, b, out=c)c = a + b 快 30%——"out 参数省掉一次内存分配,循环中差距会累积。"


1. ufunc 内部原理

ufunc(universal function)是 NumPy 对逐元素运算的统一抽象:输入一个或多个数组,逐元素调用同一个 C 函数,输出同形状数组。

(1) ufunc 执行流程

100%
graph LR
    A["输入数组"] --> B["类型提升<br>dtype promotion"]
    B --> C["广播<br>broadcasting"]
    C --> D["预分配输出<br>或使用 out"]
    D --> E["C 循环逐元素计算"]
    E --> F["输出数组"]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

(2) 内置 ufunc 分类

类别 常见 ufunc 运算符映射
算术 np.add, np.subtract, np.multiply, np.divide, np.power + - * / **
比较 np.equal, np.not_equal, np.less, np.greater == != < >
逻辑 np.logical_and, np.logical_or, np.logical_not, np.logical_xor & `
位运算 np.bitwise_and, np.bitwise_or, np.left_shift & `
三角 np.sin, np.cos, np.tan, np.arcsin
取整 np.floor, np.ceil, np.rint, np.round
数学 np.sqrt, np.exp, np.log, np.abs

每个 ufunc 都是 np.ufunc 的实例:type(np.add)<class 'numpy.ufunc'>


2. out / where / dtype 参数

ufunc 通用签名:ufunc(x1, x2, ..., out=None, where=True, dtype=None)

参数 说明
out 输出数组,避免临时分配,原地写入
where 布尔掩码,只计算 True 位置
dtype 强制输出类型,覆盖类型提升
casting 类型转换安全等级,默认 'safe'

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:out 参数(难度⭐⭐)

PYTHON
import numpy as np

a = np.arange(5, dtype=np.float64)
b = np.ones(5, dtype=np.float64)

c = np.empty(5, dtype=np.float64)
np.add(a, b, out=c)
print(c)  # [1. 2. 3. 4. 5.]

np.multiply(a, 2, out=c)
print(c)  # [0. 2. 4. 6. 8.]

result = np.empty(5, dtype=np.float64)
np.add(a, b, out=result)
np.multiply(result, 3, out=result)
print(result)  # [ 3.  6.  9. 12. 15.]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:where 条件运算(难度⭐⭐)

PYTHON
import numpy as np

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

result = np.zeros(5)
np.add(a, b, out=result, where=(a > 0))
print(result)  # [11.  0. 33.  0. 55.]

result2 = np.zeros(5)
# NumPy 2.x:np.where 不再接受 out=,改用 numpy.copyto
mask = a > 0
np.copyto(result2, np.where(mask, a, 0))
print(result2)  # [1. 0. 3. 0. 5.]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

(3) out vs 无 out 性能对比

场景 c = a + b np.add(a, b, out=c) 差异
单次运算 分配新数组 + 计算 跳过分配,直接写入 out 略快
循环 10 万次 10 万次分配 + GC 复用同一数组 out 快 2~3 倍
大数组 (1GB) 额外 1GB 临时内存 0 额外内存 out 省内存
PYTHON
import numpy as np
import time

a = np.random.randn(10_000_000)
b = np.random.randn(10_000_000)
c = np.empty_like(a)

N = 100

t0 = time.perf_counter()
for _ in range(N):
    c = a + b
t_no_out = time.perf_counter() - t0

t0 = time.perf_counter()
for _ in range(N):
    np.add(a, b, out=c)
t_out = time.perf_counter() - t0

print(f"Without out: {t_no_out:.3f}s")
print(f"With out:    {t_out:.3f}s")
print(f"Speedup:     {t_no_out / t_out:.2f}x")
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

3. reduce / accumulate / outer / at

每个 ufunc 都自带 4 个方法,用于归约、累积、外积和原地定点操作。

(1) 方法一览

方法 签名 功能
ufunc.reduce(a, axis) → scalar / array 沿轴归约,只留一个结果
ufunc.accumulate(a, axis) → same shape 沿轴累积,保留中间结果
ufunc.outer(a, b) → (len(a), len(b)) 外积,所有组合
ufunc.at(a, indices, b) → None (原地) 无缓冲原地操作

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:reduce / accumulate(难度⭐⭐)

PYTHON
import numpy as np

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

print(np.add.reduce(a))         # 15
print(np.multiply.reduce(a))    # 120

print(np.add.accumulate(a))     # [ 1  3  6 10 15]
print(np.multiply.accumulate(a))  # [  1   2   6  24 120]

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

print(np.add.reduce(b, axis=0))   # [5 7 9]
print(np.add.reduce(b, axis=1))   # [ 6 15]
print(np.add.accumulate(b, axis=0))
# [[1 2 3]
#  [5 7 9]]
print(np.add.accumulate(b, axis=1))
# [[ 1  3  6]
#  [ 4  9 15]]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:outer(难度⭐⭐)

PYTHON
import numpy as np

x = np.array([1, 2, 3])
y = np.array([10, 20])

print(np.add.outer(x, y))
# [[11 21]
#  [12 22]
#  [13 23]]

print(np.multiply.outer(x, y))
# [[10 20]
#  [20 40]
#  [30 60]]

print(np.subtract.outer(x, y))
# [[ -9 -19]
#  [ -8 -18]
#  [ -7 -17]]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

(2) reduce vs accumulate vs outer

方法 输入 输出形状 典型用途
reduce (N,) () 标量 求和、求积
accumulate (N,) (N,) 前缀和、前缀积
outer (M,), (N,) (M, N) 乘法表、距离矩阵

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:ufunc.at 原地操作(难度⭐⭐⭐)

PYTHON
import numpy as np

a = np.zeros(5)
indices = np.array([0, 1, 1, 3, 3, 3])

np.add.at(a, indices, 1)
print(a)  # [1. 2. 0. 3. 0.]

b = np.zeros(5)
np.add.at(b, [1, 3], [10, 20])
print(b)  # [ 0. 10.  0. 20.  0.]

counts = np.zeros(10, dtype=int)
data = np.array([3, 1, 4, 1, 5, 9, 2, 6, 5, 3])
np.add.at(counts, data, 1)
print(counts)  # [0 2 1 2 1 2 1 0 0 1]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

4. 自定义 ufunc:frompyfunc / vectorize

Python 函数不能直接对数组逐元素运算。frompyfuncvectorize 将其包装为 ufunc。

(1) frompyfunc

np.frompyfunc(func, nin, nout) 将任意 Python 函数转为 ufunc,返回 Python 对象数组。

PYTHON
import numpy as np

def my_func(x, y):
    return x ** 2 + y

uf = np.frompyfunc(my_func, 2, 1)

a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
print(uf(a, b))  # [11 24 39]

result = uf(a, b).astype(float)
print(result)  # [11. 24. 39.]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

(2) vectorize

np.vectorize(func, otypes=None) 类似 frompyfunc,但可指定输出类型,支持广播,有缓存优化。

PYTHON
import numpy as np

def sigmoid(x):
    return 1.0 / (1.0 + np.exp(-x))

vec_sigmoid = np.vectorize(sigmoid, otypes=[float])

a = np.array([-2, -1, 0, 1, 2])
print(vec_sigmoid(a))
# [0.1192 0.2689 0.5    0.7311 0.8808]

def clip(x, lo, hi):
    return lo if x < lo else hi if x > hi else x

vec_clip = np.vectorize(clip, otypes=[float])
data = np.array([-3, 0.5, 1, 5])
print(vec_clip(data, 0, 2))  # [0.  0.5 1.  2. ]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

(3) frompyfunc vs vectorize

特性 frompyfunc vectorize
输出类型 始终 object 可指定 otypes
性能 略快(无类型检查) 略慢(有缓存/类型推断)
广播
多输出 nout > 1
文档字符串 保留原函数 docstring
推荐场景 简单包装、需要多输出 需要正确 dtype、更友好

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:frompyfunc 自定义运算(难度⭐⭐⭐)

PYTHON
import numpy as np

def safe_div(a, b):
    if b == 0:
        return float('inf')
    return a / b

uf_div = np.frompyfunc(safe_div, 2, 1)

a = np.array([1.0, 2.0, 3.0])
b = np.array([2.0, 0.0, 1.0])

result = uf_div(a, b).astype(float)
print(result)  # [ 0.5  inf  3. ]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

5. 广播与类型提升

ufunc 在计算前自动执行两步:类型提升 + 广播。

(1) 类型提升规则

PYTHON
import numpy as np

a = np.array([1], dtype=np.int32)
b = np.array([1.0], dtype=np.float64)

result = np.add(a, b)
print(result.dtype)  # float64

print(np.result_type(np.int16, np.int32))   # int32
print(np.result_type(np.int32, np.float32))  # float32
print(np.result_type(np.float32, np.complex64))  # complex64
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

提升优先级:bool < int8 < int16 < int32 < int64 < float16 < float32 < float64 < complex64 < complex128

(2) 用 dtype 强制类型

PYTHON
import numpy as np

a = np.array([1.0, 2.0, 3.0], dtype=np.float64)
b = np.array([4.0, 5.0, 6.0], dtype=np.float64)

# NumPy 2.x:dtype kwarg 由 casting= 取代;典型做法是先相加再 astype
result = np.add(a, b).astype(np.int32)
print(result)       # [5 7 9]
print(result.dtype)  # int32
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

6. 综合示例:ufunc 方法实现滑动窗口统计

Alice 用 ufunc 的 reduce/accumulate 方法实现高效滑动窗口统计,避免 Python 循环。

PYTHON
import numpy as np

rng = np.random.default_rng(42)
data = rng.integers(0, 100, size=20)
print(f"Data: {data}")

# ========================================
# Part 1: 滑动窗口求和 — accumulate 法
# ========================================
print("\n=== Part 1: Sliding Sum via accumulate ===\n")

window = 5
prefix_sum = np.add.accumulate(data)
print(f"Prefix sum: {prefix_sum}")

sliding_sum = np.empty(len(data) - window + 1, dtype=np.int64)
sliding_sum[0] = prefix_sum[window - 1]
np.subtract(prefix_sum[window:], prefix_sum[:-window], out=sliding_sum[1:])
print(f"Sliding sum (w={window}): {sliding_sum}")

expected = np.convolve(data, np.ones(window, dtype=int), mode='valid')
print(f"Verify (convolve):        {expected}")
print(f"Match: {np.array_equal(sliding_sum, expected)}")

# ========================================
# Part 2: 滑动窗口最大值 — outer + reduce
# ========================================
print("\n=== Part 2: Sliding Max via outer ===\n")

n = len(data)
k = n - window + 1
idx = np.arange(k)
window_idx = idx[:, None] + np.arange(window)
window_data = data[window_idx]

sliding_max = np.maximum.reduce(window_data, axis=1)
print(f"Sliding max (w={window}): {sliding_max}")

# ========================================
# Part 3: 分组计数 — ufunc.at
# ========================================
print("\n=== Part 3: Group Count via ufunc.at ===\n")

categories = data % 5
counts = np.zeros(5, dtype=int)
np.add.at(counts, categories, 1)
print(f"Category counts: {counts}")
print(f"Sum of counts:   {counts.sum()} (expect {len(data)})")

# ========================================
# Part 4: 条件替换 — where 参数
# ========================================
print("\n=== Part 4: Conditional via where ===\n")

result = data.copy()
np.maximum(data, 50, out=result, where=(data < 50))
print(f"Capped at 50: {result}")

# ========================================
# Part 5: 自定义 ufunc — frompyfunc
# ========================================
print("\n=== Part 5: Custom ufunc ===\n")

def zscore(x, mean, std):
    if std == 0:
        return 0.0
    return (x - mean) / std

uf_zscore = np.frompyfunc(zscore, 3, 1)

mean_val = data.mean()
std_val = data.std()
normalized = uf_zscore(data, mean_val, std_val).astype(float)
print(f"Mean: {mean_val:.2f}, Std: {std_val:.2f}")
print(f"Z-score (first 5): {normalized[:5].round(3)}")
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

❓ 常见问题

Q ufunc 和普通函数区别?
A ufunc 是 NumPy 注册的逐元素运算函数,自动支持广播、类型提升和 out/where 等参数,底层由 C 循环驱动。普通 Python 函数对数组无这些能力,需手动循环或用 frompyfunc/vectorize 包装。
Q out 真能加速吗?
A 单次运算差异微小;但在循环中反复调用时,out 避免了每次分配新数组和触发 GC,在大数组循环场景下可快 2~3 倍,还能节省峰值内存。
Q vectorize 真的向量化吗?
A 不是。np.vectorize 只是语法糖,底层仍是 Python 逐元素循环,没有 C 级加速。它方便代码简洁,但性能与手写 for 循环相当。真要加速需用内置 ufunc 或写 C 扩展。
Q frompyfunc 和 vectorize 怎么选?
A frompyfunc 更底层,返回 object 数组需手动转换,支持多输出;vectorize 更友好,可指定输出 dtype 保存类型,推荐大多数场景使用 vectorize,需要多输出时用 frompyfunc。
Q ufunc 的 reduce 和 ndarray 的方法 reduce 有什么区别?
A np.add.reduce(a) 是 ufunc 方法,沿指定轴归约,只支持已注册的 ufunc(如 add、multiply);a.reduce() 不存在——ndarray 没有 reduce 方法。Python 内置 functools.reduce 是通用归约但只对一维且无 C 加速。ufunc.reduce 由 C 实现,支持 axis 参数,速度最快。
Q ufunc.at 和直接索引赋值有何不同?
A a[indices] += values 在重复索引时只保留最后一次赋值(有缓冲),而 np.add.at(a, indices, values) 对重复索引累加(无缓冲)。需要正确处理重复索引时必须用 at。

📖 小节


📝 作业

(1) out 优化循环运算

  1. 创建两个大数组 a = np.random.randn(1_000_000)b = np.random.randn(1_000_000)
  2. time.perf_counter 对比以下两种方式循环 1000 次的耗时:
    • c = a + b(每次新分配)
    • np.add(a, b, out=c)(预分配 c,原地写入)
  3. 打印两种耗时和加速比

(2) outer 乘法表

  1. np.arange(1, 10) 创建 1~9 的数组
  2. np.multiply.outer 生成九九乘法表(9×9)
  3. 打印结果,验证 result[2][4] 等于 3×5=15

(3) vectorize 让 Python 函数支持数组

  1. 定义 Python 函数 def grade(score): — 90 及以上返回 'A',80~89 返回 'B',70~79 返回 'C',其余 'D'
  2. np.vectorize 包装为支持数组的函数
  3. np.array([95, 82, 73, 55, 88]) 调用,打印结果
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏