对于个人站长或技术开发者来说,在处理大量日志数据、传感器数据或爬虫结果时,经常需要在VPS或虚拟机上使用Python和NumPy来构建数据集。NumPy数组的核心优势在于其固定大小和连续内存存储,但这也意味着它们并非天然支持高效的动态增长。
尝试使用传统方法(如在循环中反复使用 numpy.append 或 numpy.concatenate)来“动态”地构建大型数组,会导致极度的内存浪费和性能下降,因为每次操作都会创建并复制整个数组的新副本。
本文将介绍两种高效构建大型数据集的方法:List累积转换(最常用且高效)和NumPy内存映射(Memory Mapping)(适用于超大型数据集)。
1. 为什么传统的NumPy追加是低效的?
当你在一个循环中追加数据时,NumPy需要为新数组找到一块新的、更大的连续内存区域,并将旧数据和新数据全部复制过去。这不仅消耗大量的CPU时间,还会暂时占用双倍甚至多倍于最终数组的内存。
import numpy as np
import time
# 错误且低效的方法示例
def inefficient_append(num_elements):
data_array = np.array([], dtype=np.int32)
start_time = time.time()
for i in range(num_elements):
# 每次循环都创建一个新的更大的数组副本
data_array = np.append(data_array, i)
end_time = time.time()
print(f"低效追加耗时: {end_time - start_time:.4f} 秒")
inefficient_append(10000)
# 输出结果会显示较高的耗时,随着元素数量增加,耗时呈几何级增长
2. 最佳实践:使用Python List累积然后一次性转换
实现“动态”增长同时节省内存和时间的最简单且最高效的方法是利用Python的内置列表(List)来累积数据。Python List在内部优化了内存管理,允许高效追加。当所有数据收集完毕后,只需一步将其转换为NumPy数组。
import numpy as np
import time
# 最佳实践:使用Python List累积
def efficient_list_conversion(num_elements):
data_list = []
start_time = time.time()
# 步骤一:在循环中高效地追加到 List
for i in range(num_elements):
data_list.append(i)
# 步骤二:一次性将 List 转换为 NumPy 数组
final_array = np.array(data_list, dtype=np.int32)
end_time = time.time()
print(f"List 累积后转换耗时: {end_time - start_time:.4f} 秒")
print(f"最终数组形状: {final_array.shape}")
efficient_list_conversion(10000)
3. 内存映射(Memory Mapping)技术:处理超大型数组
如果你的数据集太大,以至于无法完全加载到VPS的RAM中(例如几十GB甚至上百GB的数据),你可以利用NumPy的内存映射(np.memmap)功能。这使得操作系统可以将文件视为内存的一部分,只在需要时将数据块加载到RAM中。这对于在资源受限的公有云虚拟机上处理大数据非常有用。
import numpy as np
import os
FILE_NAME = 'large_data.mmap'
NUM_ELEMENTS = 10**7 # 1000万个元素
# 确保文件不存在,否则会加载旧数据
if os.path.exists(FILE_NAME):
os.remove(FILE_NAME)
# 1. 创建并初始化内存映射文件
# dtype='float32' 比默认的float64节省一半内存
array_shape = (NUM_ELEMENTS,)
# mode='w+' 表示读/写并创建新文件
mmap_array = np.memmap(FILE_NAME, dtype='float32', mode='w+', shape=array_shape)
# 2. 像使用普通NumPy数组一样写入数据
# 注意:写入操作可能会导致磁盘I/O
print(f"正在写入 {NUM_ELEMENTS} 个元素到磁盘...")
for i in range(NUM_ELEMENTS):
mmap_array[i] = i * 0.5
# 3. 刷新数据到磁盘并关闭文件句柄
mmap_array.flush()
del mmap_array
# 4. 重新加载并读取数据
# mode='r' 表示只读
reloaded_array = np.memmap(FILE_NAME, dtype='float32', mode='r', shape=array_shape)
print(f"读取前10个元素: {reloaded_array[:10]}")
# 注意:只有在访问时,对应的数据块才会被加载到内存中,极大地节省了RAM空间。
汤不热吧