为什么需要对象序列化?
在开始介绍 Python Pickle 之前,先来理解一下对象序列化为何如此重要。
您也许在想:为什么不能直接把数据结构保存到文本文件里,需要时再读出来,而非一定要序列化?
让我们通过一个简单示例来理解序列化的好处。
下面是一个嵌套字典,包含学生的姓名、年龄与年级等信息:
students = {
'Student 1': {
'Name': "Alice", 'Age' :10, 'Grade':4,
},
'Student 2': {
'Name':'Bob', 'Age':11, 'Grade':5
},
'Student 3': {
'Name':'Elena', 'Age':14, 'Grade':8
}
}
让我们检查一下“students”对象的数据类型:
type(students)
dict
既然已确认该对象是字典类型,接下来不进行序列化,直接把它写入文本文件:
with open('student_info.txt','w') as data:
data.write(str(students))
请注意,文本文件只能写入字符串对象,因此我们使用 str() 将字典转换为字符串。这意味着字典的原始状态已经丢失。
现在,读回该字典、打印并再次检查其类型:
with open("student_info.txt", 'r') as f:
for students in f:
print(students)
type(students)
str
这个嵌套字典现在只是以字符串的形式被打印出来,当我们尝试访问其键或值时会报错。
这正是序列化派上用场的地方。
在处理更复杂的数据类型(如字典、数据框、嵌套列表)时,序列化可以让我们保留对象的原始状态,不丢失任何相关信息。
本文稍后将学习如何使用 Pickle 将同一个字典存入文件,并使用该库将其反序列化。您也可以在另一篇教程中进一步阅读Python 字典推导式。
Python 中的 Pickle 简介
Python 的 Pickle 模块是用于序列化与反序列化数据类型的常用格式。该格式是 Python 原生的,这意味着用 Pickle 序列化的对象无法用其他编程语言直接加载。
与其他序列化格式相比,Pickle 既有优势,也存在一些不足。
使用 Pickle 序列化对象的优点
- 与不能很好处理元组和 datetime 对象的 JSON 等格式不同,Pickle 几乎可以序列化所有常见的 Python 内置数据类型。它还能保留对象的精确状态,这是 JSON 做不到的。
- 在存储递归结构时,Pickle 也是不错的选择,因为它对同一对象只写入一次。
- Pickle 在反序列化时更灵活。您可以轻松将不同的变量保存到一个 Pickle 文件中,并在另一个 Python 会话里加载它们,原封不动地恢复数据,而无需修改代码。
使用 Pickle 的缺点
- Pickle 不安全,因为它会执行构造对象所需的任意 Python 可调用对象。在反序列化时,Pickle 无法区分恶意与非恶意的可调用对象。因此,用户在反序列化过程中可能会执行任意代码。
- 如前所述,Pickle 是特定于 Python 的模块,在使用其他语言时,您可能无法顺利反序列化 Pickle 对象。
- 多项基准测试显示,Pickle 通常比 JSON 和 Apache Thrift 更慢,且生成的序列化结果更大。
使用 Pickle 的 dump 与 load 函数保存和加载对象
Pickle 使用以下函数来序列化与反序列化 Python 对象:
pickle.dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None)
pickle.dumps(obj, protocol=None, *, fix_imports=True, buffer_callback=None)
pickle.load(file, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)
pickle.loads(data, /, *, fix_imports=True, encoding=”ASCII”, errors=”strict”, buffers=None)
Pickle 的 dump() 与 dumps() 用于序列化对象。它们的唯一区别在于:dump() 将数据写入文件,而 dumps() 返回一个字节对象。
类似地,load() 从文件中读取 pickled 对象,而 loads() 则从类字节对象中反序列化。
在本教程中,我们将使用 dump() 和 load() 将 Python 对象 pickle 到文件并进行解包(unpickle)。
用 Pickle 序列化 Python 数据结构
列表(Lists)
先创建一个简单的 Python 列表:
import pickle
student_names = ['Alice','Bob','Elena','Jane','Kyle']
现在,打开一个文件,使用 dumps() 将该列表写入并关闭文件:
with open('student_file.pkl', 'wb') as f: # open a text file
pickle.dump(student_names, f) # serialize the list
我们首先创建了名为“student_file.pkl”的文件。扩展名不一定必须是 .pkl,您可以任意命名,文件都会被创建。不过,使用 .pkl 作为扩展名是个良好习惯,能提醒您这是一个 Pickle 文件。
另请注意,我们以 wb 模式打开文件。这表示以二进制方式写入文件,使数据以字节对象的形式返回。
随后,我们使用 dump() 函数将 student_names 列表存入文件。
最后,您可以使用以下代码关闭文件:
f.close()
现在,让我们反序列化该文件并打印列表:
with open('student_file.pkl', 'rb') as f:
student_names_loaded = pickle.load(f) # deserialize using load()
print(student_names_loaded) # print student names
上述代码的输出应如下所示:
['Alice', 'Bob', 'Elena', 'Jane', 'Kyle']
请注意,为了反序列化文件,我们需要使用表示二进制读取的 rb 模式。然后,使用 load() 进行解包,并将数据存入另一个变量中以便后续使用。
现在检查刚刚解包后的列表的数据类型:
type(student_names_loaded)
list
很好!我们保留了该列表的原始状态与数据类型。
Numpy 数组
接下来试试序列化和反序列化更复杂一些的数据类型——Numpy 数组。
先创建一个 10×10 的全 1 数组:
import numpy as np
numpy_array = np.ones((10,10)) # 10x10 array
然后,与之前相同,调用 dump() 将该数组序列化到文件:
with open('my_array.pkl','wb') as f:
pickle.dump(numpy_array, f)
最后,将该数组解包,并检查其形状与数据类型,确保保留了原始状态:
with open('my_array.pkl','rb') as f:
unpickled_array = pickle.load(f)
print('Array shape: '+str(unpickled_array.shape))
print('Data type: '+str(type(unpickled_array)))
您应得到如下输出:
Array shape: (10, 10)
Data type: <class 'numpy.ndarray'>
解包后的对象是一个 10×10 的 Numpy 数组,其形状与数据类型与我们序列化前一致。
pandas DataFrame
数据框是数据科学家日常工作的核心对象。加载和保存 Pandas DataFrame 最常见的方法是读写 CSV 文件。想进一步了解导入数据,请参阅我们的pandas read_csv() 教程。
然而,这一过程通常比序列化更慢,若数据框很大则会非常耗时。
让我们通过比较用 Pickle 与 CSV 保存、加载 pandas DataFrame 所需的时间,来对比两者的效率。
首先,创建一个包含 100,000 行伪数据的 pandas DataFrame:
import pandas as pd
import numpy as np
# Set random seed
np.random.seed(123)
data = {'Column1': np.random.randint(0, 10, size=100000),
'Column2': np.random.choice(['A', 'B', 'C'], size=100000),
'Column3': np.random.rand(100000)}
# Create Pandas dataframe
df = pd.DataFrame(data)
现在,计算将该 DataFrame 保存为 csv 文件所用的时间:
import time
start = time.time()
df.to_csv('pandas_dataframe.csv')
end = time.time()
print(end - start)
0.19349145889282227
我们用了 0.19 秒将一个有三列、10 万行的 Pandas DataFrame 保存为 csv 文件。
再看看使用 Pickle 是否能提升性能。pandas 提供了 to_pickle() 方法,仅用一行代码即可将 DataFrame 序列化为 pickle 文件:
start = time.time()
df.to_pickle("my_pandas_dataframe.pkl")
end = time.time()
print(end - start)
0.0059659481048583984
将同一个 Pandas DataFrame 保存为 Pickle 文件仅耗时 5 毫秒,与保存为 csv 相比有显著提升。
现在把文件再读回 Pandas,比较读取 Pickle 与读取 csv 的速度:
# Reading the csv file into Pandas:
start1 = time.time()
df_csv = pd.read_csv("my_pandas_dataframe.csv")
end1 = time.time()
print("Time taken to read the csv file: " + str(end1 - start1) + "\n")
# Reading the Pickle file into Pandas:
start2 = time.time()
df_pkl = pd.read_pickle("my_pandas_dataframe.pkl")
end2 = time.time()
print("Time taken to read the Pickle file: " + str(end2 - start2))
上述代码应输出如下结果:
Time taken to read the csv file: 0.00677490234375
Time taken to read the Pickle file: 0.0009608268737792969
将 CSV 读入 Pandas 用时 6 毫秒,而读取 Pickle 仅用 0.9 毫秒。
虽看起来差距不大,但对大型 Pandas DataFrame 使用 Pickle 进行序列化可显著节省时间。Pickle 还能在所有情况下保留每一列的数据类型,且通常比 CSV 占用更少磁盘空间。
字典(Dictionaries)
最后,让我们序列化教程第一部分写入文本文件的那个字典:
students = {
'Student 1': {
'Name': "Alice", 'Age' :10, 'Grade':4,
},
'Student 2': {
'Name':'Bob', 'Age':11, 'Grade':5
},
'Student 3': {
'Name':'Elena', 'Age':14, 'Grade':8
}
}
回顾一下,当我们将该字典保存为文本文件时,不得不将其转换为字符串,结果丢失了原始状态。
现在使用 Pickle 对其进行序列化并读回,确保它仍保留 Python 字典的全部属性:
# serialize the dictionary to a pickle file
with open("student_dict.pkl", "wb") as f:
pickle.dump(students, f)
# deserialize the dictionary and print it out
with open("student_dict.pkl", "rb") as f:
deserialized_dict = pickle.load(f)
print(deserialized_dict)
您应得到如下输出:
{'Student 1': {'Name': 'Alice', 'Age': 10, 'Grade': 4}, 'Student 2': {'Name': 'Bob', 'Age': 11, 'Grade': 5}, 'Student 3': {'Name': 'Elena', 'Age': 14, 'Grade': 8}}
接着检查该变量的类型:
type(deserialized_dict)
dict
尝试访问该字典中第一位学生的一些信息:
print(
"The first student's name is "
+ deserialized_dict["Student 1"]["Name"]
+ " and she is "
+ (str(deserialized_dict["Student 1"]["Age"]))
+ " years old."
)
The first student's name is Alice and she is 10 years old.
很好!该字典保留了全部原始属性,访问方式与序列化前完全一致。想了解更多,请参阅我们的Python 字典推导式教程。
用 Pickle 序列化机器学习模型
训练一个机器学习模型通常很耗时,可能需要数小时甚至数天。在需要复用或迁移到其他环境时,从零开始重训并不现实。
如果您想了解构建机器学习算法的最佳实践,可以学习我们的 Python 课程用 Python 设计机器学习工作流。
Pickle 可以让您将机器学习模型按现有状态序列化,从而在需要时再次使用。
本节将学习如何用 Pickle 序列化机器学习模型。
为此,先生成一些伪数据,并用 Scikit-Learn 构建一个线性回归模型:
from sklearn.linear_model import LinearRegression
from sklearn.datasets import make_regression
# generate regression dataset
X, y = make_regression(n_samples=100, n_features=3, noise=0.1, random_state=1)
# train regression model
linear_model = LinearRegression()
linear_model.fit(X, y)
现在打印部分模型概览参数:
# summary of the model
print('Model intercept :', linear_model.intercept_)
print('Model coefficients : ', linear_model.coef_)
print('Model score : ', linear_model.score(X, y))
Model intercept : -0.010109549594702116
Model coefficients : [44.18793068 98.97389468 58.17121618]
Model score : 0.9999993081899219
随后使用 Pickle 的 dump() 函数对该模型进行序列化:
with open("linear_regression.pkl", "wb") as f:
pickle.dump(linear_model, f)
模型现已保存为 Pickle 文件。使用 load() 将其反序列化:
with open("linear_regression.pkl", "rb") as f:
unpickled_linear_model = pickle.load(f)
序列化的模型已加载到 unpickled_linear_model 变量。检查其参数,确保与最初创建的模型一致:
# summary of the model
print('Model intercept :', unpickled_linear_model.intercept_)
print('Model coefficients : ', unpickled_linear_model.coef_)
print('Model score : ', unpickled_linear_model.score(X, y))
您应得到如下输出:
Model intercept : -0.010109549594702116
Model coefficients : [44.18793068 98.97389468 58.17121618]
Model score : 0.9999993081899219
很好!我们刚刚解包的模型参数与最初创建的完全一致。
接下来,您可以使用该模型对测试数据集进行预测、在其基础上继续训练,或将其迁移至其他环境。
为大型对象提升 Python Pickle 的性能
在多项基准测试中,Pickle 往往比 JSON、XML 和 HDF5 更高效。
但在处理极其庞大的数据结构或巨大的机器学习模型时,Pickle 也可能显著变慢,序列化会成为工作流中的瓶颈。
以下是一些减少保存与加载 Pickle 文件耗时的方法:
使用 PROTOCOL 参数
当前保存与加载 Pickle 文件的默认协议版本是 4,它在不同 Python 版本间具有最佳兼容性。
不过,如果您希望加速工作流,可以使用 HIGHEST_PROTOCOL,这是 Pickle 可用的最快协议。
为比较 Pickle 最兼容协议与默认协议之间的性能差异,先用默认协议序列化一个 Pandas DataFrame。注意:若不显式指定,Pickle 会使用默认协议版本:
import pickle
import time
import numpy as np
# Set random seed
np.random.seed(100)
data = {'Column1': np.random.randint(0, 10, size=100000),
'Column2': np.random.choice(['A', 'B', 'C'], size=100000),
'Column3': np.random.rand(100000)}
# serialize to a file
start = time.time()
with open("df1.pkl", "wb") as f:
pickle.dump(data, f)
end = time.time()
print(end - start)
0.006001710891723633
使用 Pickle 默认协议序列化该 DataFrame 大约耗时 6 毫秒。
现在使用最高协议进行序列化:
start = time.time()
with open("df2.pkl", "wb") as f:
pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL)
end = time.time()
print(end - start)
0.0030384063720703125
使用最高协议后,我们将序列化时间缩短了一半。
使用 cPickle 替代 Pickle
cPickle 模块是用 C 语言实现的更快版本,因此比纯 Python 实现的 Pickle 库更快。
注意,在 Python 3 中,cPickle 更名为 _pickle,我们将导入后者。
import _pickle as cPickle
start = time.time()
with open("df3.pkl", "wb") as f:
cPickle.dump(data, f)
end = time.time()
print(end-start)
0.004027366638183594
使用 cPickle 序列化约耗时 4 毫秒,相较 Python 版 Pickle 有明显提升。
只序列化必要内容
即便采用加速手段,对于大型对象,序列化仍可能很慢。
要提升性能,您可以将数据结构拆分,仅序列化必要的子集。
例如在处理字典时,可以只保留后续需要访问的键值对;在序列化前先缩减字典规模,以降低对象复杂度,从而显著加快过程。
用 Python 的 Pickle 进行序列化:下一步
恭喜您!您已经学习了与 Python 序列化及 Pickle 库相关的一系列主题。
现在,您应已清楚什么是序列化、如何使用 Pickle 序列化 Python 数据结构,以及如何通过不同参数与模块优化 Pickle 的性能。
以下是一些可进一步加深理解、并将其用于提升数据科学工作流的步骤:
- 学习其他序列化格式。 本教程仅介绍了 Pickle,但它并非在所有场景下都是最佳选择。值得学习 JSON、XML、HDF5 等其他格式,以便在不同用例中做出恰当选择。
- 亲手实践。 将本教程的概念应用到您的数据科学工作流中。下次创建新数据结构或将计算结果存储到变量时,请将其序列化以便后续复用,而不是一遍遍重新运行全部代码。
- 参加在线课程。 要优化数据科学工作流并高效完成序列化等任务,必须深入理解 Python 的数据结构与对象。为此,您可以学习我们的Python 数据科学数据类型课程。
Natassha 是一名在数据科学与营销交汇处工作的数据顾问。她相信,明智地使用数据,能够为个人和组织带来巨大的增长。作为一名自学成才的数据从业者,Natassha 热衷于撰写文章,帮助有志于数据科学的人士进入该行业。她在个人博客以及外部平台发表的文章,平均每月获得 20 万次浏览。

掌握序列化非常有用。它能让您保留数据或模型的状态,避免每次都从头处理或重新训练。
本文将带您了解广为人知的 Python Pickle 模块。读完后,您将学会如何序列化与反序列化常见数据结构(如列表和字典),并掌握不同的性能优化技巧。
我也推荐接下来学习我们的用 Python 设计机器学习工作流课程,在那里您将学习如何将机器学习模型序列化并容器化以投产。
对象序列化简介
是否厌倦了每次需要访问先前创建的数据框、变量或机器学习模型时,都要重新运行一遍 Python 代码?
对象序列化或许正是您要找的解决方案。
它指的是将数据结构以当前状态存储在内存或磁盘上的过程,以便在需要时加载或传输而不丢失其现有状态。
下面是一张简单的示意图,解释序列化的工作方式:
图片由作者提供
在 Python 中,我们使用列表、元组、集合等高级数据结构。但是,当我们想将这些对象持久化存储时,需要将其转换为计算机能够理解的字节序列。这个过程称为序列化。
当我们下次要访问同一数据结构时,必须将该字节序列重新转换为高级对象,这个过程称为反序列化。
可用于序列化的格式包括 JSON、XML、HDF5 和 Pickle。本教程将学习用于序列化的 Python Pickle 库。我们会介绍其使用方法,并讨论何时应选择 Pickle 而非其他序列化格式。
最后,我们将学习如何使用 Python 的 Pickle 库来序列化列表、字典、Pandas 数据框、机器学习模型等。
若想更轻松地亲自运行本教程中的所有示例代码,您可以免费创建一个 DataLab 工作簿,其中预装了 Python 并包含全部代码示例。若想进一步练习加载 Python 的 pickled 文件,请查看这道 DataCamp 动手练习。