用Pickle进行Python对象序列化的方法

469 阅读7分钟

对象序列化是一个迷人的编程概念,在Python中使用Pickle库就可以轻松实现。Pickle提供了一组内置的函数和功能,使转储和加载对象到文件或从文件中转储都是小菜一碟。


什么是对象序列化?

序列化是将一个对象转换为字节流的过程,其反面是将字节流转换为Python对象。

更简单地说,对象序列化是将实际的Python对象转换为字节的过程,允许整个对象被保留下来(包括它的所有当前值)。这通常被称为 "pickling "或 "dumping",在这里我们把字节流保存到一个文件中。

这个过程的反面是我们将这些字节转换回 Python 对象。

对象序列化在很多情况下都是超级有用的,比如创建保存文件来存储诸如游戏数据或AI/机器学习问题的训练模型。人工智能算法生成一个模型可能需要很长的时间,所以与其每次运行程序时都这样做,不如先把它转储到一个文件中,然后每次都从那里读取,可能会使程序的速度提高100倍。


倾倒(腌制)对象

在这一节中,我们将探讨如何将 Python 中的对象转储 (序列化) 到二进制文件。

例子# 1

你所要做的就是用一个小小的文件处理方法,以二进制模式打开一个文件,然后使用pickle.dump() 函数。

为了以二进制模式打开文件,第二个参数中必须有一个 "b"。比如用 "w "代替打开普通文件的参数,用 "wb "代替向二进制文件写入的参数,以此类推。记住,文件不会有扩展名,所以不必费力地包括一个扩展名。

pickle.dump() 方法需要两个参数,第一个是要被提取/转储的对象,第二个是文件流

import pickle

mydict = { "Apple": 20, "Meat" : 100, "Bread": 10 }

ofile = open("BinaryData",'wb')
pickle.dump(mydict,ofile)
ofile.close()

如果你打开这个数据被转储的二进制文件,你会看到类似于下图的东西。

Python Pickle Object Serialization - Binary Data

正如你所看到的,这其实没有什么意义,因为它是以二进制格式存储的。当你试图把它读成文本时,它就会变得很奇怪。但不要担心,它不需要对我们有意义。它的唯一目的是被保存在那里,直到我们准备好去读它。


例子# 2

在这个例子中,我们来看看如何创建一个自定义类的对象,然后将它们转储到二进制文件中。

我们简单地创建了三个学生类的对象,把它们添加到列表中,然后把整个列表转储到二进制文件中。把所有的数据放在一个数据结构中,使得来回转储和加载数据都变得更加容易。

class Student:
    def __init__(self, Id, Name):
        self.Id = Id
        self.Name = Name

    def display(self):
        print("ID:", self.Id)
        print("Name:", self.Name)


list1 = [Student(1,"Bob"), Student(2,"Sam"), Student(3,"James")]

ofile = open("BinaryData2",'wb')
pickle.dump(list1,ofile)
ofile.close()

你也可以选择将每个学生单独转储到BinaryData 文件中。然而,这可能有点复杂,所以我们将在本教程中稍后再次提出这个问题。


加载(取消挑剔)对象

在这一节中,我们将探讨如何在Python中加载(Unpickle)对象,从二进制文件返回到Pythonic对象。

例子# 1

所以我们将继续我们之前的例子,向你展示如何从二进制文件中读取数据。我们在这里创建了一个全新的文件,我们将在这里编写我们的加载代码。

你需要做的就是以二进制读取模式("rb")打开该文件,然后使用pickle.load() 函数。这个函数只需要一个参数,也就是我们之前打开的文件流。

import pickle

ifile = open("BinaryData",'rb')
mydict2 = pickle.load(ifile)
ifile.close()

print(mydict2["Apple"])
print(mydict2["Bread"])
print(mydict2["Meat"])

这将产生以下输出。

20
10
100

例#2

在这里,我们将做与例#1相同的事情,将例#2中的数据读回Python列表中。

由于我们转储了/pickled类学生的对象,我们现在可以对它们进行迭代,并调用display()函数来验证一切是否按计划进行。

import pickle

ifile = open("BinaryData2",'rb')
list2 = pickle.load(ifile)
ifile.close()

for student in list2:
    student.display()

输出结果。

ID: 1
Name: Bob
ID: 2
Name: Sam
ID: 3
Name: James

一个文件中的多个泡菜

你可能想知道是否有可能在一个二进制文件中制作多个pickles(多个dump)。是的,这是有可能的,正如下面的例子所示。

你转储它们的顺序将是它们被读取的顺序(否则它们会被混在一起)。下面的例子很清楚地证明了这一点。

class Student:
    def __init__(self, Id, Name):
        self.Id = Id
        self.Name = Name

    def display(self):
        print("ID:", self.Id)
        print("Name:", self.Name)


s1 = Student(1, "Bob")
s2 = Student(2, "Sam")
s3 = Student(3, "James")

ofile = open("BinaryData3",'wb')
pickle.dump(s1,ofile)
pickle.dump(s2,ofile)
pickle.dump(s3,ofile)
ofile.close()

ifile = open("BinaryData3",'rb')
s4 = pickle.load(ifile)
s5 = pickle.load(ifile)
s6 = pickle.load(ifile)
ifile.close()

s4.display()
s5.display()
s6.display()

输出。

ID: 1
Name: Bob
ID: 2
Name: Sam
ID: 3
Name: James

然而,在一个文件中制作多个泡菜可能有点混乱,因为你需要跟踪泡菜的顺序或泡菜的数量。一个更简单的解决方案是将所有的数据包裹在一个容器中,如列表或字典,然后转储到一个文件中。

这也是我们在本节之前的例子中一直使用的方法。


不可挑选的类型

尽管Pickle很厉害,但它不能 "腌制 "所有东西。有一些东西是Pickle无法转储的,如果你尝试的话,会抛出一个错误。UnPickable Types的数量相当少,而且它们往往是非常复杂的结构。像这样的情况其实很少发生,所以不用太担心。

Pickle可以Pickle的东西列表。

  • None,True, 和False
  • 整数、浮点数、复数
  • 字符串、字节、字节数
  • 元组、列表、集合和只包含可拾取对象的字典
  • 定义在模块顶层的函数(使用def ,而不是lambda )。
  • 在模块顶层定义的内置函数
  • 在模块顶层定义的类
  • 这些类的实例,其__dict__ 或调用__getstate__() 的结果是可提取的。

替代品。

  1. 迪尔库。这是Pickle库的一个特殊扩展,语法和功能大致相同。它能够比原来的Pickle库更多的结构类型,如嵌套函数。但它不能100%地处理所有的情况,所以它不是一个完美的解决方案。

压缩Pickle转储(奖励)

作为一个小小的奖励,我们还将解释如何压缩我们已经转储的Pickle文件。Pickle转储都是二进制格式,所以压缩起来相当容易和简单。

我们所需要的是使用bzip2压缩库,它将为我们处理所有的艰苦工作。我们只需要调用正确的函数,我们的压缩文件就会准备好。

下面是我们将使用的代码。它和以前一样,但用一个二维列表代替,它有100个原始列表的副本,有三个学生对象。这样做的原因是为了使非压缩版本和压缩版本之间的差异明显。否则由于开销的原因,如果数据非常小,非压缩版本的大小有可能更小。

class Student:
    def __init__(self, Id, Name):
        self.Id = Id
        self.Name = Name

    def display(self):
        print("ID:", self.Id)
        print("Name:", self.Name)


list1= [Student(1,"Bob"),Student(2,"Sam"),Student(3,"James")] * 100

我们现在将尝试用常规的文件处理方法bz2 方法来转储文件,然后用操作系统库的 getsize() 方法来比较文件的大小。

ofile = open("BinaryData2",'wb')
pickle.dump(list1,ofile)
ofile.close()
print(os.path.getsize("BinaryData2"))

文件的大小。

722

现在让我们用bz2 来试一下。

ofile = bz2.BZ2File("BinaryData2",'wb')
pickle.dump(list1,ofile)
ofile.close()
print(os.path.getsize("BinaryData2"))

文件的大小。

155

使用bz2 来压缩数据,结果是几乎5倍的改进。而这仅仅是个开始。数据集越大,压缩的效果就越明显,可以使文件缩小10倍-30倍(根据内容而定)。

**注意:**上述测试的结果可能略有偏差,因为相同的数据被写入文件100次。如果数据是相似的,压缩效率通常会更好(由于压缩技术的工作原理)。

如果你想了解更多关于bz2 和它的各种压缩和解压功能,请看这个bz2(bzip2)教程