NumPy高效保存与加载字典:np.savez实战与性能对比

发布时间:2026/8/2 13:17:17
NumPy高效保存与加载字典:np.savez实战与性能对比 1. 为什么需要numpy来保存字典在日常的Python数据处理中字典dict是我们最常用的数据结构之一它灵活、高效能存储键值对。然而当数据量变大或者字典里嵌套了复杂的数值数组比如numpy数组时传统的保存方法就开始显得力不从心了。你可能试过用Python内置的pickle模块来保存字典。这确实很方便一个pickle.dump()就能搞定。但pickle有几个硬伤第一它生成的二进制文件是Python特有的其他语言比如C、Java基本没法直接读取第二当字典里包含大型numpy数组时pickle的序列化效率并不高文件体积也可能偏大第三pickle在加载不可信来源的文件时有安全风险。你也可能试过用json但它只能处理基本的数据类型列表、字典、字符串、数字等一旦遇到numpy数组它就直接报错了。这时numpy自带的.npy和.npz格式就派上用场了。它们是为高效存储numpy数组而生的二进制格式读写速度极快文件体积小并且是跨平台的。虽然numpy本身主要面向数组但通过一些技巧我们可以巧妙地用它来保存和加载字典尤其是当字典的值是numpy数组时这种方法几乎是“原生”且最优的选择。简单来说如果你字典里存的是大量数值数据用numpy来存既能享受高性能又能保证数据的“纯洁性”避免在pickle和纯文本格式之间来回转换的麻烦。2. 核心方法np.savez 与 np.savez_compressednumpy提供了两个直接用于保存多个数组的函数np.savez和np.savez_compressed。它们是我们实现字典保存功能的基础。这两个函数的工作原理不是把整个字典当成一个对象存起来而是把字典“拆开”将每个键值对作为一个独立的numpy数组保存到一个.npz文件中。np.savez是最基本的版本。你调用np.savez(‘data.npz’, **your_dict)它就会遍历字典把每个值必须是或可转换为数组以键名作为标识符保存起来。生成的文件是一个压缩包实际是ZIP格式里面包含了多个.npy文件每个文件对应字典里的一个键。np.savez_compressed是它的增强版顾名思义它在保存时会使用ZIP压缩算法进一步减小文件的磁盘占用空间。这对于包含大型数组的字典来说节省的空间非常可观。压缩和解压需要额外的CPU时间但对于现代计算机和通常的数值数据规模几百MB到几个GB这个开销在IO节省面前往往是值得的。我个人的经验法则是只要不是对读写速度有极端要求比如高频实时处理一律使用np.savez_compressed。这里有一个关键点字典的“值”必须能被numpy识别并转换为数组。numpy数组、列表、元组、标量数字都没问题。但如果值是字符串、复杂的自定义对象或者另一个字典直接保存就会出错。对于这些非数值型数据我们需要在保存前进行预处理这个我们后面会详细讲。3. 实战第一步保存一个简单的数值字典让我们从一个最直接的例子开始。假设我们有一个字典里面存储了一些实验参数和对应的结果数组。import numpy as np # 创建一个示例字典 data_dict { ‘experiment_id‘: ‘exp_20240527‘, ‘temperature‘: 25.5, ‘pressure‘: 101.3, ‘timestamps‘: np.array([0.0, 1.0, 2.0, 3.0, 4.0]), ‘readings‘: np.random.randn(5, 100) # 一个5x100的随机数据矩阵 }现在我们想把这个字典保存起来。注意‘experiment_id‘是一个字符串‘temperature‘和‘pressure‘是浮点数‘timestamps‘和‘readings‘是numpy数组。如果我们直接调用np.savez_compressed字符串会出问题。# 错误的尝试会报错 # np.savez_compressed(‘experiment_data.npz‘, **data_dict) # TypeError: Object dtype dtype(‘O‘) has no native HDF5 equivalent错误信息提示我们对象类型这里是Python字符串没有对应的本地存储格式。所以我们需要先处理非数值型数据。一个常见的策略是将它们转换为numpy数组的某种形式。对于字符串我们可以用np.array(obj, dtype‘object‘)将其包装成对象数组但这在后续读取和跨环境使用时可能不稳定。更稳妥的做法是将这些元数据字符串、标量与主要的数值数据分开处理或者将它们转换为可存储的格式。为了简化演示我们假设experiment_id也需要保存。我们可以这样做# 正确的保存步骤 def save_dict_to_npz(filepath, data_dict): 将字典保存为.npz文件。 自动将字符串等非数值类型转换为字节串存储。 processed_dict {} for key, value in data_dict.items(): # 检查值的类型 if isinstance(value, (np.ndarray, list, tuple, int, float, np.number)): # 数值、列表、元组、numpy数组等直接存储 processed_dict[key] np.asarray(value) elif isinstance(value, str): # 字符串类型转换为numpy的字节串数组S类型 # 使用‘S‘字节串而非‘U‘Unicode兼容性更好 processed_dict[key] np.array(value, dtype‘S‘) else: # 其他复杂类型建议先转换为字符串或pickle字节流 # 这里为了简单转换为字符串。生产环境需更严谨。 processed_dict[key] np.array(str(value), dtype‘S‘) print(f“Warning: Key ‘{key}‘ with type {type(value)} was converted to string.“) # 使用压缩格式保存 np.savez_compressed(filepath, **processed_dict) print(f“字典已保存至: {filepath}“) # 使用函数保存 save_dict_to_npz(‘my_experiment.npz‘, data_dict)这段代码的核心在于np.asarray(value)它会尝试将输入转换为numpy数组。对于标量数字会生成一个0维数组。对于字符串我们显式地指定dtype‘S‘将其存储为字节串。保存后你会得到一个my_experiment.npz文件。4. 读取与还原从.npz文件到字典保存之后读取就相对简单了。np.load函数是打开.npz文件的钥匙。它返回一个类似字典的NpzFile对象。但这里有一个至关重要的细节为了节省内存np.load默认采用的是“懒加载”模式。这意味着当你加载.npz文件时数据并没有立即读入内存只有当你通过键名去访问具体的数组时该数组才会被加载。# 加载.npz文件 loaded_data np.load(‘my_experiment.npz‘, allow_pickleTrue) # allow_pickleTrue允许加载对象数组 # 查看里面有哪些键 print(f“文件中的键: {list(loaded_data.keys())}“) # 访问一个数组此时它才被加载到内存 timestamps loaded_data[‘timestamps‘] print(f“timestamps: {timestamps}“) # 将整个NpzFile对象转换为真正的字典 # 注意这会立即将所有数组加载到内存中 full_dict {key: loaded_data[key] for key in loaded_data.files} print(f“字典中的readings形状: {full_dict[‘readings‘].shape}“) # 非常重要关闭文件对象 loaded_data.close()注意allow_pickleTrue这个参数。当我们保存了字符串作为对象数组时加载就需要这个参数。但它也带来了和pickle类似的安全隐患。因此只加载你信任的来源生成的.npz文件。如果你能确保字典里全是纯数值数组那么可以设置allow_pickleFalse这样更安全。现在full_dict几乎还原了我们保存的字典。但是之前被转换成字节串的字符串现在还是numpy的字节串类型如b‘exp_20240527‘。我们需要将它们转换回Python字符串。def load_dict_from_npz(filepath): 从.npz文件加载并还原字典。 自动将字节串转换回字符串。 with np.load(filepath, allow_pickleTrue) as data: result_dict {} for key in data.files: value data[key] # 如果值是字节串数组dtype以‘S‘开头将其解码为字符串 if value.dtype.kind ‘S‘: # ‘S‘ 表示字节串 # 对于0维数组标量直接解码对于多维数组使用列表推导 if value.ndim 0: result_dict[key] value.item().decode(‘utf-8‘) else: # 这里简化处理假设是一维字符串数组 result_dict[key] [v.decode(‘utf-8‘) for v in value] else: # 对于数值数据直接使用。如果原来是Python标量可以用item()取出 if value.ndim 0 and isinstance(value.item(), (int, float, np.number)): result_dict[key] value.item() else: result_dict[key] value return result_dict # 使用函数加载 restored_dict load_dict_from_npz(‘my_experiment.npz‘) print(f“还原后的experiment_id: {restored_dict[‘experiment_id‘]}“) print(f“还原后的temperature类型: {type(restored_dict[‘temperature‘])}“)使用with语句可以确保文件在使用后被正确关闭。解码字节串时使用‘utf-8‘编码是最通用的选择。经过这样的加载和转换我们就能得到一个和原始字典非常接近的Python字典了。5. 处理复杂字典嵌套字典与非数组对象现实中的数据往往更复杂。字典里可能嵌套着字典值可能是列表的列表或者自定义类的实例。numpy的.npz格式对此无能为力因为它本质上是为同质化的数值数组设计的。面对这种情况我们有几种策略。策略一展平字典如果嵌套不深可以将嵌套字典展平用复合键名来表示层级关系。例如{‘params‘: {‘lr‘: 0.01, ‘epochs‘: 10}}可以展平为{‘params_lr‘: 0.01, ‘params_epochs‘: 10}。保存和读取的代码不需要大变只是键名变了。这种方法简单但破坏了原有的数据结构读取后需要额外逻辑来重组。策略二结合pickle这是处理任意Python对象最强大的方法。思路是先用pickle将整个字典序列化成字节流然后将这个字节流作为一个numpy数组dtype‘object‘或‘S‘保存到.npz文件中。这样我们既利用了.npz的格式又拥有了pickle的序列化能力。import pickle def save_dict_pickle_npz(filepath, data_dict): 将字典通过pickle序列化后存入npz文件。 # 将字典pickle化为字节流 pickled_bytes pickle.dumps(data_dict) # 将字节流转换为numpy数组无符号8位整数数组即字节数组 byte_array np.frombuffer(pickled_bytes, dtypenp.uint8) # 保存这个字节数组 np.savez_compressed(filepath, pickled_databyte_array) def load_dict_pickle_npz(filepath): 从npz文件中加载pickle化的字典。 with np.load(filepath, allow_pickleTrue) as data: byte_array data[‘pickled_data‘] # 将字节数组还原为字节流 pickled_bytes byte_array.tobytes() # 反序列化 restored_dict pickle.loads(pickled_bytes) return restored_dict # 测试一个复杂字典 complex_dict { ‘config‘: {‘model‘: ‘CNN‘, ‘layers‘: [32, 64, 128]}, ‘metadata‘: {‘author‘: ‘Alice‘, ‘date‘: ‘2024-05-27‘}, ‘custom_object‘: SomeCustomClass() # 假设这是一个自定义类实例 } save_dict_pickle_npz(‘complex_data.npz‘, complex_dict) loaded_complex load_dict_pickle_npz(‘complex_data.npz‘) print(loaded_complex[‘config‘][‘layers‘]) # 输出: [32, 64, 128]这种方法非常通用可以保存任何pickle能处理的对象。但缺点也同样明显你失去了.npz格式的透明性和部分性能优势加载后的数据不再是纯粹的numpy数组而是原始的Python对象。而且pickle的安全性和版本兼容性问题依然存在。策略三分层存储对于混合了大型数组和少量元数据的场景一个折中的方案是分开存储。将大型的numpy数组用.npz保存将复杂的元数据字典、列表等用json或pickle保存为另一个文件。读取时分别加载再合并。这样既保证了大型数组的存取效率又保持了元数据的结构。import json def save_mixed_data(npz_path, json_path, data_dict): 分离存储数组存npz其他存json。 array_dict {} meta_dict {} for key, value in data_dict.items(): if isinstance(value, np.ndarray): array_dict[key] value else: # 尝试JSON序列化失败则提示 try: # 临时转换检查是否可序列化 json.dumps(value) meta_dict[key] value except (TypeError, OverflowError): print(f“Key ‘{key}‘ is not JSON serializable and will be skipped.“) np.savez_compressed(npz_path, **array_dict) with open(json_path, ‘w‘) as f: json.dump(meta_dict, f, indent2) # 加载时也分别加载 def load_mixed_data(npz_path, json_path): with np.load(npz_path) as data: array_data {k: data[k] for k in data.files} with open(json_path, ‘r‘) as f: meta_data json.load(f) # 合并字典注意json.load的数字可能会变成int/float与原来的类型一致 return {**array_data, **meta_data}选择哪种策略取决于你的数据特点和对未来数据使用的预期。如果数据以数值数组为主偶尔有字符串标签用第一节的方法即可。如果数据结构复杂但体积不大用picklenpz最省事。如果数据是海量数组加一些配置分层存储更清晰、高效。6. 性能对比与格式选型建议不同的保存方法在速度、文件大小和易用性上差异很大。我做了一个简单的测试用一个包含10个键的字典做基准每个键对应一个1000x1000的随机浮点数数组约8MB。np.savez_compressed(纯数组字典)保存速度快。直接调用底层C库写入压缩数据。文件大小最小。得益于高效的二进制格式和压缩。读取速度快。懒加载模式按需读取内存友好。易用性中。需要预处理非数值数据读取后需类型转换。适用场景字典值主要为numpy数组或数值数据。这是性能最优、最推荐的方式。pickle.dump(原生Python)保存速度中等。Python序列化过程有开销。文件大小较大。尤其是包含numpy数组时格式不够紧凑。读取速度中等。需要完整的反序列化。易用性高。一行代码保存一行代码读取完美还原任何对象。适用场景数据结构复杂、包含自定义对象、对性能要求不极端、仅在Python环境内使用。np.savez_compressedpickle(混合策略)性能特征介于两者之间。保存和读取多了一次pickle序列化/反序列化的开销文件体积会比纯npz大因为pickle的元数据但比纯pickle小因为数组部分还是用高效格式存储。适用场景需要保存复杂对象但又希望文件体积相对较小且不介意pickle的安全限制。json 手动数组编码 (如Base64)保存速度慢。需要将二进制数组编码为文本。文件大小巨大。文本编码会使数据膨胀约33%。读取速度慢。需要解码文本。易用性低。编解码麻烦仅支持基本类型。适用场景需要人类可读、或必须使用纯文本格式的极端情况。通常不推荐用于存储数组数据。我的选型建议首选np.savez_compressed只要你的字典核心是数值数组就优先采用第3节的方法处理好字符串等边缘类型。这是性能和通用性的最佳平衡点。慎用pickle除非数据结构极其复杂且确定只在特定Python环境间流转否则尽量避免。如果要用考虑pickle协议版本如protocol4或5以获得更好的性能和兼容性。避免重复造轮子对于非常复杂的、嵌套的、包含非数值类型的数据结构如果对性能要求不是特别高直接使用pickle可能是最经济的。如果对文件大小有要求可以考虑使用joblib库来自scikit-learn它对包含大数组的Python对象如字典的序列化做了优化本质上也是pickle但更智能。考虑HDF5如果你的项目已经涉及科学计算并且数据量非常大、结构需要更复杂的组织比如分组、属性那么直接使用h5py库操作HDF5文件是比.npz更强大、更标准的选择。HDF5可以原生存储字典式的结构并且支持压缩、分块等高级特性。7. 常见踩坑点与排查指南在实际操作中你肯定会遇到各种报错。这里我总结几个最常见的坑和解决办法。坑1TypeError: Object dtype dtype(‘O‘) has no native HDF5 equivalent问题这个错误通常在尝试保存非数值类型如字符串、列表、字典时出现。.npz底层在某些实现或与HDF5交互时期望的是数值类型。排查在保存前遍历字典检查每个值的类型。使用print(key, type(value), value)来定位问题键。解决按照第3节的方法将字符串转换为字节串dtype‘S‘将列表/元组转换为numpy数组np.asarray(value)。对于其他复杂对象考虑采用第5节的策略二或三。坑2加载后得到字节串b‘...‘而不是字符串问题这是最普遍的情况。numpy将字符串存储为字节串加载后自然也是字节串。解决在加载函数中增加类型判断和解码步骤如第4节的load_dict_from_npz函数所示。使用value.dtype.kind ‘S‘来判断是否为字节串然后用.decode(‘utf-8‘)解码。坑3ValueError: Cannot load file containing pickled data when allow_pickleFalse问题你保存时包含了对象数组比如用dtypeobject保存了字符串但加载时设置了allow_pickleFalse。这是numpy的安全机制。解决如果你确认文件来源安全将allow_pickle参数设为True。如果可能尽量在保存阶段就避免产生对象数组转而使用字节串dtype‘S‘这样加载时就可以安全地使用allow_pickleFalse。坑4内存不足特别是加载大型.npz文件时问题使用{key: data[key] for key in data.files}这样的字典推导式会立即将所有数组加载到内存。如果文件很大会导致内存溢出。排查检查.npz文件大小和系统可用内存。解决懒加载直接使用np.load返回的NpzFile对象像字典一样按需访问键。只有访问到的数组才会被加载。分块处理如果单个数组就很大考虑在保存时就将其分割成多个小块分别存储。读取时也分批处理。使用内存映射对于非常大的数组可以考虑使用numpy.memmap但这需要以特定的.npy格式存储单个数组不太适合.npz包。这时HDF5可能是更好的选择。坑5跨平台/跨Python版本兼容性问题问题在Windows上保存的文件在Linux上读取时或者用Python 3.8保存用Python 3.11读取时可能遇到字节序或pickle协议不兼容的问题。解决对于纯数值数组.npy/.npz格式通常是跨平台兼容的。如果涉及pickle无论是直接使用还是通过对象数组间接使用尽量保证两端Python版本接近并使用较低的、兼容性好的pickle协议如protocol2但会损失一些新特性。最根本的解决方法是避免依赖pickle。将数据转换为平台无关的格式如数值用npz文本用json。坑6键名冲突或无效问题字典的键在保存时会被用作npz文件内部的文件名。如果键名包含特殊字符如/,:或过长在某些系统上可能会出问题。解决在保存前对键名进行简单的清洗例如将非字母数字字符替换为下划线。或者使用一个安全的映射关系将原始键映射到简单的键名如key_0,key_1同时将映射关系另外保存。8. 一个完整的工业级示例与封装最后我将分享一个我自己在项目中封装好的工具函数。它处理了类型转换、异常处理、懒加载选项等细节你可以直接拿去用。import numpy as np import pickle from typing import Any, Dict class NpzDictHandler: 一个健壮的、用于使用numpy的npz格式保存和加载字典的工具类。 自动处理字符串、标量、嵌套对象通过pickle与数组的混合存储。 staticmethod def save(filepath: str, data_dict: Dict[str, Any], compress: bool True, use_pickle_for_complex: bool False): 保存字典到.npz文件。 参数: filepath: 保存路径。 data_dict: 要保存的字典。 compress: 是否使用压缩格式。 use_pickle_for_complex: 是否对无法直接存储的复杂对象使用pickle。 如果为False复杂对象将被忽略并警告。 save_func np.savez_compressed if compress else np.savez np_dict {} pickled_dict {} for key, value in data_dict.items(): # 尝试作为numpy数组或标量存储 if isinstance(value, (np.ndarray, list, tuple, int, float, np.integer, np.floating)): np_dict[key] np.asarray(value) elif isinstance(value, str): # 存储为固定长度的字节串避免对象类型 # 注意长字符串可能会被截断对于超长字符串建议用pickle np_dict[key] np.array(value, dtype‘S‘) elif isinstance(value, (bytes, bytearray)): np_dict[key] np.frombuffer(value, dtypenp.uint8) else: # 其他类型 if use_pickle_for_complex: # 标记此键需要pickle并存入另一个字典 pickled_dict[key] value else: print(f“Warning: Key ‘{key}‘ with type {type(value)} cannot be saved directly and ‘use_pickle_for_complex‘ is False. Skipping.“) # 如果有需要pickle的对象将其序列化后作为一个单独的数组存储 if pickled_dict: try: pickled_bytes pickle.dumps(pickled_dict, protocolpickle.HIGHEST_PROTOCOL) np_dict[‘__pickled_data__‘] np.frombuffer(pickled_bytes, dtypenp.uint8) except Exception as e: print(f“Error pickling complex objects: {e}. They will not be saved.“) if not np_dict: raise ValueError(“No savable data found in the dictionary.“) save_func(filepath, **np_dict) print(f“Dictionary saved to {filepath} (compressed: {compress})“) staticmethod def load(filepath: str, lazy: bool False) - Dict[str, Any]: 从.npz文件加载字典。 参数: filepath: 文件路径。 lazy: 如果为True返回一个支持懒加载的类字典对象。 如果为False立即将所有数据加载到内存并返回普通字典。 返回: 还原后的字典。 # 必须允许pickle因为可能包含pickle数据 npz_file np.load(filepath, allow_pickleTrue) if lazy: # 返回一个包装器实现懒加载 class LazyNpzDict: def __init__(self, npz_file): self._npz_file npz_file self._cache {} self._pickled_data_loaded False self._pickled_data None def __getitem__(self, key): if key in self._cache: return self._cache[key] # 先检查是否是pickle数据的键 if not self._pickled_data_loaded and ‘__pickled_data__‘ in self._npz_file.files: self._load_pickled_data() if self._pickled_data and key in self._pickled_data: value self._pickled_data[key] self._cache[key] value return value # 否则从npz文件加载 if key in self._npz_file.files and key ! ‘__pickled_data__‘: value self._npz_file[key] # 类型还原 value NpzDictHandler._convert_value(value) self._cache[key] value return value raise KeyError(key) def _load_pickled_data(self): byte_arr self._npz_file[‘__pickled_data__‘] pickled_bytes byte_arr.tobytes() self._pickled_data pickle.loads(pickled_bytes) self._pickled_data_loaded True def keys(self): all_keys set(self._npz_file.files) all_keys.discard(‘__pickled_data__‘) if ‘__pickled_data__‘ in self._npz_file.files: if not self._pickled_data_loaded: self._load_pickled_data() all_keys.update(self._pickled_data.keys()) return all_keys def __contains__(self, key): return key in self.keys() def close(self): self._npz_file.close() def __enter__(self): return self def __exit__(self, *args): self.close() return LazyNpzDict(npz_file) else: # 立即加载所有数据 result {} # 先处理可能的pickle数据 if ‘__pickled_data__‘ in npz_file.files: byte_arr npz_file[‘__pickled_data__‘] pickled_bytes byte_arr.tobytes() pickled_dict pickle.loads(pickled_bytes) result.update(pickled_dict) # 处理常规数据 for key in npz_file.files: if key ‘__pickled_data__‘: continue value npz_file[key] result[key] NpzDictHandler._convert_value(value) npz_file.close() return result staticmethod def _convert_value(np_value): 将加载出的numpy值转换回Python原生类型或数组。 # 如果是字节串类型 if np_value.dtype.kind ‘S‘: if np_value.ndim 0: return np_value.item().decode(‘utf-8‘, errors‘ignore‘) else: # 处理多维字节串数组简化版返回列表 return [v.decode(‘utf-8‘, errors‘ignore‘) for v in np_value.flat] # 如果是标量数值 elif np_value.ndim 0: return np_value.item() # 其他情况多为数组直接返回 else: return np_value # 使用示例 if __name__ ‘__main__‘: # 创建一个混合字典 my_data { ‘array_data‘: np.arange(12).reshape(3, 4), ‘scalar_int‘: 42, ‘scalar_float‘: 3.14159, ‘string_label‘: ‘experiment_01‘, ‘list_data‘: [1, 2, 3], ‘nested_dict‘: {‘a‘: 1, ‘b‘: 2}, # 复杂对象需要pickle } # 保存启用复杂对象pickle NpzDictHandler.save(‘test_data.npz‘, my_data, compressTrue, use_pickle_for_complexTrue) # 方式1立即加载全部 loaded_all NpzDictHandler.load(‘test_data.npz‘, lazyFalse) print(“立即加载:“) print(f“ array_data shape: {loaded_all[‘array_data‘].shape}“) print(f“ nested_dict: {loaded_all[‘nested_dict‘]}“) # 方式2懒加载 with NpzDictHandler.load(‘test_data.npz‘, lazyTrue) as lazy_loaded: print(“\n懒加载 (仅访问部分键):“) # 只有被访问的键才会真正加载数据 print(f“ scalar_int: {lazy_loaded[‘scalar_int‘]}“) print(f“ string_label: {lazy_loaded[‘string_label‘]}“) # 嵌套字典是从pickle数据中加载的 print(f“ nested_dict: {lazy_loaded[‘nested_dict‘]}“)这个封装类NpzDictHandler提供了几个关键特性自动类型处理智能判断数据类型选择最合适的存储方式。可选复杂对象支持通过use_pickle_for_complex参数控制是否对嵌套字典等对象使用pickle。懒加载支持当数据量很大时可以使用lazyTrue返回一个代理对象只有在访问具体键时才加载数据极大节省内存。错误处理与警告对无法处理的数据类型给出明确警告。资源管理使用with语句或手动close()来确保文件句柄被正确关闭。你可以根据自己项目的需求对这个类进行修改和扩展。比如增加对pandas DataFrame的支持或者集成压缩级别选择等。希望这个从原理到实践从基础到封装的完整梳理能帮你彻底掌握用numpy高效保存和读取字典的技巧。