4、数据结构选型:List vs. Array vs. NumPy ndarray的性能对比与适用场景
做量化因子开发,说白了就是在跟数据打交道。我见过不少新手,上来就用Python原生的List一把梭,结果回测跑起来慢得像蜗牛。也有老手,不管三七二十一全转成NumPy,结果处理一些简单的增删操作反而把自己绕进去了。
今天我们就来聊聊这三种数据结构——List、Array、NumPy ndarray。我会结合我踩过的坑,帮你理清楚什么时候该用谁。
4.1 先看底层:它们到底有什么区别?
先给个直观的对比表,你一看就明白:
| 特性 | List | array.array | NumPy ndarray |
|---|---|---|---|
| 元素类型 | 任意类型混装 | 单一类型(需指定) | 单一类型(自动推断) |
| 内存布局 | 指针数组(碎片化) | 连续内存块 | 连续内存块(支持strides) |
| 运算速度 | 慢(Python循环) | 中等(C底层但无向量化) | 极快(向量化+SIMD) |
| 适用场景 | 小数据、频繁增删 | 中等数据、需类型约束 | 大规模数值计算 |
为什么会这样?我简单解释一下。
List 里存的其实不是数据本身,而是指向数据对象的指针。每个元素都是一个独立的Python对象,有引用计数、类型信息这些额外开销。你想想看,一个float在List里占24字节,在NumPy里只占8字节。这就是差距。
array.array 是标准库里的东西,它把数据老老实实地排成一排,类型统一。但问题是,它没有向量化运算。你做个加法,还得自己写循环。
NumPy ndarray 就不一样了。它底层用C实现,支持广播、向量化,还能利用CPU的SIMD指令集。我在项目中遇到过,同样的因子计算,用List要跑3秒,换成NumPy只要0.05秒。差了60倍。
4.2 性能实测:一个简单的因子计算
我们拿一个常见的因子——动量因子来测试。假设你有100万条收盘价数据,计算过去20天的平均涨幅。
import time
import array
import numpy as np
# 模拟数据
n = 1_000_000
prices_list = [100.0 + i*0.01 for i in range(n)]
prices_array = array.array('d', prices_list)
prices_np = np.array(prices_list, dtype=np.float64)
# List版本
start = time.time()
result_list = []
for i in range(20, n):
avg = sum(prices_list[i-20:i]) / 20
result_list.append(avg)
print(f"List耗时: {time.time() - start:.3f}秒")
# Array版本
start = time.time()
result_array = array.array('d')
for i in range(20, n):
avg = sum(prices_array[i-20:i]) / 20
result_array.append(avg)
print(f"Array耗时: {time.time() - start:.3f}秒")
# NumPy版本
start = time.time()
window = np.lib.stride_tricks.sliding_window_view(prices_np, 20)
result_np = window.mean(axis=1)
print(f"NumPy耗时: {time.time() - start:.3f}秒")
跑出来的结果大概是:
- List:约 4.2 秒
- Array:约 3.8 秒
- NumPy:约 0.08 秒
嗯,这里要注意。Array虽然底层是连续内存,但求和操作还是Python级别的循环,所以快得有限。NumPy用了滑动窗口视图,加上C级别的向量化求和,速度直接起飞。
核心结论: 凡是涉及数值计算、矩阵运算、统计聚合,无脑选NumPy。List和Array在计算密集型任务面前,基本没有还手之力。
4.3 什么时候该用List?
你可能会问:那List是不是就没用了?当然不是。
我个人习惯在以下几种场景用List:
- 数据量小(几千条以内),性能差异可以忽略
- 频繁增删元素,比如实时行情流处理
- 元素类型混杂,比如既有字符串又有数字
- 快速原型开发,先跑通逻辑再优化
我记得有一次做因子探索,数据只有5000条,我用List写了个简单的均线策略。同事非要用NumPy重构,结果花了半小时改代码,性能提升不到0.1秒。你说值不值?
小技巧: 如果你不确定数据量级,先用List写。等确定要上生产环境了,再考虑换成NumPy。过早优化是万恶之源。
4.4 Array的尴尬位置
说实话,array.array在量化场景里有点尴尬。它比List快一点,但远不如NumPy。它比NumPy轻量,但功能又太少。
我曾经在一个嵌入式量化设备上用过Array——因为那台设备装不了NumPy。但如果你能装NumPy,就别折腾Array了。
唯一值得提的是,如果你需要把数据传给C扩展或者做二进制序列化,Array的buffer协议支持得比较好。但这种情况在量化里不多见。
4.5 选型决策流程图
我画了一张图,帮你快速决策:
4.6 避坑指南
我曾经犯过的错: 有一次我把因子数据存在List里,然后循环里反复做切片和求和。数据量只有20万条,但回测跑了整整15分钟。后来换成NumPy的rolling window,15秒就搞定了。嗯,从那以后我写任何数值计算代码,第一反应就是NumPy。
还有几个坑你要注意:
- 不要用List存大量浮点数做计算——内存爆炸,速度感人
- 不要用Array做矩阵运算——它根本没有矩阵运算的概念
- 不要频繁在NumPy和List之间转换——转换本身有开销
- 注意NumPy的dtype——默认float64,如果你精度要求不高,用float32能省一半内存
4.7 总结一下
说白了,选型就三条原则:
- 计算密集型 → NumPy ndarray
- 小数据+灵活操作 → List
- 特殊场景(无NumPy环境) → array.array
我个人习惯是:因子开发阶段全用NumPy,因为计算快、代码简洁。等到做数据清洗、日志记录这些非核心逻辑时,才用List。至于Array,我一年也用不了几次。
记住一句话:选对数据结构,性能优化就成功了一半。
如果你刚开始学,建议把NumPy的向量化操作练熟。这是量化性能优化的基本功,后面讲内存布局、缓存优化的时候,你还会用到这些知识。