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 执行流程
graph LR
A["输入数组"] --> B["类型提升<br>dtype promotion"]
B --> C["广播<br>broadcasting"]
C --> D["预分配输出<br>或使用 out"]
D --> E["C 循环逐元素计算"]
E --> F["输出数组"]
> **输出:** 在本地 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' |
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:out 参数(难度⭐⭐)
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.]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:where 条件运算(难度⭐⭐)
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.]
> **输出:** 在本地 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 省内存 |
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")
> **输出:** 在本地 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 (原地) |
无缓冲原地操作 |
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:reduce / accumulate(难度⭐⭐)
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]]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:outer(难度⭐⭐)
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]]
> **输出:** 在本地 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) |
乘法表、距离矩阵 |
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:ufunc.at 原地操作(难度⭐⭐⭐)
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]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
4. 自定义 ufunc:frompyfunc / vectorize
Python 函数不能直接对数组逐元素运算。frompyfunc 和 vectorize 将其包装为 ufunc。
(1) frompyfunc
np.frompyfunc(func, nin, nout) 将任意 Python 函数转为 ufunc,返回 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.]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) vectorize
np.vectorize(func, otypes=None) 类似 frompyfunc,但可指定输出类型,支持广播,有缓存优化。
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. ]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(3) frompyfunc vs vectorize
| 特性 | frompyfunc |
vectorize |
|---|---|---|
| 输出类型 | 始终 object | 可指定 otypes |
| 性能 | 略快(无类型检查) | 略慢(有缓存/类型推断) |
| 广播 | ✅ | ✅ |
| 多输出 | ✅ nout > 1 |
❌ |
| 文档字符串 | 无 | 保留原函数 docstring |
| 推荐场景 | 简单包装、需要多输出 | 需要正确 dtype、更友好 |
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:frompyfunc 自定义运算(难度⭐⭐⭐)
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. ]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
5. 广播与类型提升
ufunc 在计算前自动执行两步:类型提升 + 广播。
(1) 类型提升规则
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
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
提升优先级:bool < int8 < int16 < int32 < int64 < float16 < float32 < float64 < complex64 < complex128
(2) 用 dtype 强制类型
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
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
6. 综合示例:ufunc 方法实现滑动窗口统计
Alice 用 ufunc 的 reduce/accumulate 方法实现高效滑动窗口统计,避免 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)}")
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
❓ 常见问题
np.vectorize 只是语法糖,底层仍是 Python 逐元素循环,没有 C 级加速。它方便代码简洁,但性能与手写 for 循环相当。真要加速需用内置 ufunc 或写 C 扩展。frompyfunc 更底层,返回 object 数组需手动转换,支持多输出;vectorize 更友好,可指定输出 dtype 保存类型,推荐大多数场景使用 vectorize,需要多输出时用 frompyfunc。np.add.reduce(a) 是 ufunc 方法,沿指定轴归约,只支持已注册的 ufunc(如 add、multiply);a.reduce() 不存在——ndarray 没有 reduce 方法。Python 内置 functools.reduce 是通用归约但只对一维且无 C 加速。ufunc.reduce 由 C 实现,支持 axis 参数,速度最快。a[indices] += values 在重复索引时只保留最后一次赋值(有缓冲),而 np.add.at(a, indices, values) 对重复索引累加(无缓冲)。需要正确处理重复索引时必须用 at。📖 小节
- ufunc 原理 — 逐元素 C 循环 + 自动广播 + 类型提升
out参数 — 复用输出数组,省内存分配,循环中显著加速where参数 — 布尔掩码,只计算指定位置reduce— 沿轴归约为单个值,如np.add.reduce求和accumulate— 沿轴累积保留中间结果,如前缀和outer— 两数组所有组合,如乘法表at— 无缓冲原地操作,重复索引正确累加frompyfunc— 将 Python 函数包装为 ufunc,返回 object 数组vectorize— 同上但可指定输出类型,更友好但非真向量化
📝 作业
(1) out 优化循环运算
- 创建两个大数组
a = np.random.randn(1_000_000)和b = np.random.randn(1_000_000) - 用
time.perf_counter对比以下两种方式循环 1000 次的耗时:c = a + b(每次新分配)np.add(a, b, out=c)(预分配 c,原地写入)
- 打印两种耗时和加速比
(2) outer 乘法表
- 用
np.arange(1, 10)创建 1~9 的数组 - 用
np.multiply.outer生成九九乘法表(9×9) - 打印结果,验证
result[2][4]等于 3×5=15
(3) vectorize 让 Python 函数支持数组
- 定义 Python 函数
def grade(score):— 90 及以上返回 'A',80~89 返回 'B',70~79 返回 'C',其余 'D' - 用
np.vectorize包装为支持数组的函数 - 对
np.array([95, 82, 73, 55, 88])调用,打印结果