跳至主要内容

Python Pickle 教程:对象序列化

了解 Python 的 pickle 模块:掌握序列化、何时(不)该使用、如何压缩已 pickle 的对象、多进程支持等更多内容!
· 12分钟 阅读

使用 AI 探索

掌握序列化非常有用。它能让您保留数据或模型的状态,避免每次都从头处理或重新训练。

本文将带您了解广为人知的 Python Pickle 模块。读完后,您将学会如何序列化与反序列化常见数据结构(如列表和字典),并掌握不同的性能优化技巧。

我也推荐接下来学习我们的用 Python 设计机器学习工作流课程,在那里您将学习如何将机器学习模型序列化并容器化以投产。

对象序列化简介

是否厌倦了每次需要访问先前创建的数据框、变量或机器学习模型时,都要重新运行一遍 Python 代码?

对象序列化或许正是您要找的解决方案。

它指的是将数据结构以当前状态存储在内存或磁盘上的过程,以便在需要时加载或传输而不丢失其现有状态。

下面是一张简单的示意图,解释序列化的工作方式:

Pickle Python Object Serialization

图片由作者提供

在 Python 中,我们使用列表、元组、集合等高级数据结构。但是,当我们想将这些对象持久化存储时,需要将其转换为计算机能够理解的字节序列。这个过程称为序列化。

当我们下次要访问同一数据结构时,必须将该字节序列重新转换为高级对象,这个过程称为反序列化。

可用于序列化的格式包括 JSON、XML、HDF5 和 Pickle。本教程将学习用于序列化的 Python Pickle 库。我们会介绍其使用方法,并讨论何时应选择 Pickle 而非其他序列化格式。

最后,我们将学习如何使用 Python 的 Pickle 库来序列化列表、字典、Pandas 数据框、机器学习模型等。

若想更轻松地亲自运行本教程中的所有示例代码,您可以免费创建一个 DataLab 工作簿,其中预装了 Python 并包含全部代码示例。若想进一步练习加载 Python 的 pickled 文件,请查看这道 DataCamp 动手练习。

为什么需要对象序列化?

在开始介绍 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 的性能。

以下是一些可进一步加深理解、并将其用于提升数据科学工作流的步骤:

  1. 学习其他序列化格式。 本教程仅介绍了 Pickle,但它并非在所有场景下都是最佳选择。值得学习 JSON、XML、HDF5 等其他格式,以便在不同用例中做出恰当选择。
  2. 亲手实践。 将本教程的概念应用到您的数据科学工作流中。下次创建新数据结构或将计算结果存储到变量时,请将其序列化以便后续复用,而不是一遍遍重新运行全部代码。
  3. 参加在线课程。 要优化数据科学工作流并高效完成序列化等任务,必须深入理解 Python 的数据结构与对象。为此,您可以学习我们的Python 数据科学数据类型课程。
Natassha Selvaraj

作者

Natassha Selvaraj

Natassha 是一名在数据科学与营销交汇处工作的数据顾问。她相信,明智地使用数据,能够为个人和组织带来巨大的增长。作为一名自学成才的数据从业者,Natassha 热衷于撰写文章,帮助有志于数据科学的人士进入该行业。她在个人博客以及外部平台发表的文章,平均每月获得 20 万次浏览。

主题