1. 什么是 pickle?不仅仅是“打包”那么简单
在 python 的世界里,我们经常需要处理各种各样的对象:从简单的列表、字典,到复杂的自定义类实例。当程序运行时,这些对象都活在内存里,一旦程序结束或计算机关机,它们就会烟消云散。如果我们想把这些对象“持久化”保存下来,或者在网络上传输,该怎么办?
这就是**序列化(serialization)**发挥作用的时刻。简单来说,序列化就是把内存中的对象转换成可以存储(存入文件)或传输(通过网络发送)的字节流的过程。而反序列化,则是把字节流重新变回对象。
python 标准库中有一个非常强大但也常被误解的模块——pickle。
很多人把 pickle 简单地理解为“打包”,这其实并不准确。虽然它确实能把多个对象“打包”成一个字节流,但它真正的核心能力在于几乎能序列化任何 python 对象。
pickle 的超能力:它到底能做什么
与其他通用的序列化格式(如 json 或 xml)相比,pickle 最显著的特点是通用性。
- json 只能处理基本类型(字符串、数字、列表、字典等)。如果你想把一个自定义的
user类实例存成 json,你必须先手动把它转换成字典。 - pickle 则不同,它可以直接处理:
- 复杂的内置类型:集合(set)、复数(complex)、元组(tuple)等。
- 自定义类和实例:它会自动记录类的定义和实例的状态。
- 函数和类:甚至可以序列化函数引用(虽然这有局限性,详见后文)。
- 循环引用:如果对象 a 引用了对象 b,对象 b 又引用了对象 a,pickle 能正确处理这种循环,而不会死循环。
简单上手:pickle 的基本用法
pickle 的 api 非常直观,核心函数主要有四个:dump(转储)、dumps(转储为字符串)、load(加载)、loads(从字符串加载)。
import pickle
# 定义一个稍微复杂的对象
class player:
def __init__(self, name, level):
self.name = name
self.level = level
def upgrade(self):
self.level += 1
# 创建实例
p1 = player("alice", 10)
data = {"players": [p1], "meta": "game_save_v1"}
# 1. 序列化到文件 (dump)
with open("save.pkl", "wb") as f: # 注意:必须以二进制写模式 'wb' 打开
pickle.dump(data, f)
# 2. 从文件反序列化 (load)
with open("save.pkl", "rb") as f: # 注意:必须以二进制读模式 'rb' 打开
loaded_data = pickle.load(f)
print(f"加载后的玩家: {loaded_data['players'][0].name}, 等级: {loaded_data['players'][0].level}")
# 输出: 加载后的玩家: alice, 等级: 10
这段代码展示了 pickle 的便捷性。p1 是一个自定义类的实例,通过 pickle,它被完美地保存并复活了。
2. 深入 pickle 的工作原理与协议版本
要真正掌握 pickle,我们需要揭开它的面纱,看看它在“打包”的过程中到底发生了什么。
pickle 虚拟机与协议
pickle 并不是简单地把对象转成字节,它其实是在执行一个基于栈的序列化语言。当你调用 pickle.dump(obj, f) 时,pickle 会把对象转换成一系列指令(opcodes),这些指令描述了如何重建该对象。当反序列化时,pickle 虚拟机读取这些指令,压入栈,最终构建出对象。
这就引出了一个重要的概念:pickle 协议(protocol)。
python 提供了 5 个主要的协议版本(0-5),通过 pickle.dump(obj, file, protocol=...) 指定。
- protocol 0:旧式的文本格式,人类可读(某种程度上),但效率低,不支持二进制数据。
- protocol 1 & 2:旧式二进制格式,效率有所提升,但仍然比较古老。
- protocol 3:python 3.0 引入,是 python 3.x 早期的默认值。引入了对字节对象的更好支持。
- protocol 4:python 3.4 引入,支持更大的对象(超过 4gb),更多现代数据类型。
- protocol 5:python 3.8 引入,引入了**带外数据(out-of-band data)**支持,允许将大块字节数据(如 numpy 数组)直接传输而不必先拷贝到 pickle 流中,极大提升了大数据处理的效率。
实用建议:除非你需要兼容非常古老的 python 2 环境,否则请始终使用 protocol=pickle.highest_protocol(或者至少 protocol 4),以获得最佳性能和功能支持。
甚至可以序列化代码
pickle 甚至可以序列化函数和类定义,但这依赖于引用而非包含。
def my_function():
return "hello"
# 序列化函数本身(实际上只保存了引用路径)
pickled_func = pickle.dumps(my_function)
# 反序列化
unpickled_func = pickle.loads(pickled_func)
# print(unpickled_func()) # 这在同一个脚本里通常能工作
这背后的逻辑是:pickle 并没有把函数的字节码打包进去,而是记录了“这个函数叫什么,在哪个模块”。当反序列化时,它会去导入那个模块里的同名函数。这意味着,如果你把一个 pickle 文件发给没有该函数定义的别人,反序列化就会失败。
3. 安全性:pickle 是最危险的“打包”工具
这是 pickle 最核心、最需要警惕的部分。永远不要 unpickle(反序列化)来自不可信来源的数据!
为什么 pickle 如此危险
json 或 xml 只是数据,它们本身不会执行任何操作。但 pickle 不同,它是一组指令。当你反序列化时,你实际上是在执行一段代码。
pickle 允许定义 __reduce__ 方法,该方法可以告诉 pickle 在反序列化时应该执行什么操作。
攻击演示(概念性代码,请勿在生产环境运行):
假设黑客构造了一个恶意的 pickle 文件,其中包含了一个恶意类:
import pickle
import os
class malicious:
def __reduce__(self):
# 当这个对象被反序列化时,会自动执行 os.system
return (os.system, ("echo '你的电脑被黑客控制了!'", ))
# 黑客生成恶意文件
with open("evil.pkl", "wb") as f:
pickle.dump(malicious(), f)
如果你不小心在你的服务器上加载了这个 evil.pkl 文件:
# 受害者代码
with open("evil.pkl", "rb") as f:
data = pickle.load(f) # 危险!此时代码已经执行!
结果就是 os.system("echo '...'") 被执行。这不仅仅是执行代码,黑客可以通过类似方式执行任何系统命令,比如删除文件、下载恶意软件、发起网络攻击等。
如何防范
- 绝对不要反序列化不可信数据:这是铁律。如果你必须接收用户上传的数据,不要用 pickle。
- 使用更安全的替代品:
- json:最通用,安全。
- messagepack:二进制版的 json,高效且相对安全。
- protobuf / flatbuffers:google 的方案,适合高性能、强类型场景。
- 如果必须用 pickle:确保数据来源经过了严格的验证(例如,通过了身份验证的内部系统之间传输),或者使用 hmac 等签名机制验证数据的完整性。
4. pickle 的局限性与调试技巧
虽然 pickle 很强大,但它并非万能。在实际开发中,我们经常会遇到一些“坑”。
4.1 无法序列化的对象
有些对象是无法被 pickle 的,通常会抛出 typeerror。
- 文件句柄/网络连接:打开的文件或 socket 连接不能序列化,因为它们代表操作系统资源,序列化它们没有意义,重启后也无法恢复连接。
- lambda 函数 / 匿名函数:pickle 无法序列化通过 lambda 表达式定义的函数,因为它没有名字,无法通过“引用路径”找到。
- 生成器(generators):生成器函数本身可以序列化,但生成器对象(处于运行状态的)通常不能,因为它们包含复杂的执行状态。
- 外部资源:如数据库连接池等。
解决方案:在自定义类中实现 __getstate__ 和 __setstate__ 方法。
class resourceholder:
def __init__(self):
self.db_connection = "模拟连接对象"
self.config = {"key": "value"}
def __getstate__(self):
# 序列化前调用:返回需要保存的字典
state = self.__dict__.copy()
# 移除不能序列化的部分
del state['db_connection']
return state
def __setstate__(self, state):
# 反序列化后调用:恢复对象状态
self.__dict__.update(state)
# 重新初始化连接
self.db_connection = "重新建立的连接"
4.2 版本兼容性噩梦
这是 pickle 最令人头疼的问题之一。
如果你的类定义发生了变化,比如:
- 修改了类名。
- 修改了模块名(文件移动)。
- 增加或删除了
__init__中的参数。
那么反序列化旧的 pickle 文件很可能会失败,或者产生意想不到的结果。
调试技巧:
- 保持类定义稳定:尽量不要随意改动已用于持久化存储的类的结构。
- 使用版本号:在 pickle 数据中包含一个版本字段,或者使用
__setstate__来处理旧版本数据的兼容逻辑。 - 使用
__reduce__自定义重建逻辑:如果类结构大改,可以通过__reduce__指定如何用新类重建旧数据。
4.3 性能问题
虽然 pickle 速度很快,但在处理海量小对象时,开销依然可观。对于科学计算(如 numpy 数组),标准 pickle 效率较低。
解决方案:
- 使用
pickle.highest_protocol。 - 对于 numpy 数组,使用
pickle.dumps时,如果协议版本足够高,它会尝试使用 numpy 自己的序列化逻辑,效率会高很多。或者直接使用 numpy 自带的.npy格式。
5. 总结与最佳实践
pickle 是 python 生态中一把锋利的瑞士军刀。它让对象的持久化变得极其简单,能够处理复杂的 python 数据结构。
核心观点总结:
- 便利与风险并存:pickle 的“能序列化一切”特性正是其最大的安全漏洞。不要反序列化不可信来源的数据。
- 协议很重要:始终使用最新的协议版本(如 protocol 4 或 5)以获得更好的性能和功能。
- 处理变动:如果数据需要长期保存,请谨慎对待类定义的变更,利用
__getstate__/__setstate__做好兼容性管理。 - 场景选择:
- 适合:临时缓存、内部系统间通信、保存复杂的 python 内部状态(如 scikit-learn 模型)。
- 不适合:用户数据交换、需要跨语言交互的场景(此时请用 json、protobuf)、长期归档存储(因为类定义可能随代码迭代而失效)。
到此这篇关于python使用pickle模块序列化对象的完整指南的文章就介绍到这了,更多相关python pickle序列化对象内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论