【Python】 python对象的文件化 pickle

时间:2021-10-19 19:38:08

pickle

  之前隐隐约约在哪里看到过pickle这个模块但一直没怎么用过。然后让我下定决心学习一下这个模块的原因竟然是【妹抖龙女(男)主在工作中用到了pickle哈哈哈】。嗯嗯,不扯皮了。pickle的作用是把Python的对象序列化为适合储存到文件、可通过网络传输、存放于数据库中的字节流方式。相当于把原先只存在于内存中的python对象给固化到硬盘上来。这个和之前用到过的shelve有点像,不过shelve只支持字典一个格式,而pickle应该是百通的。

  关于对象序列化(or you will “文件化”)的一些理论性的东西可以参见http://www.cnblogs.com/cobbliu/archive/2012/09/04/2670178.html。大体来说,序列化分成几种格式,比如文本格式,二进制格式等等。不同的序列化格式会有不同的存储性能和运行效率等,但从固化对象这个角度来讲各种格式都可以做到。

  python还有一个cPickle模块,作用基本上和pickle相同,但是具有更好的性能。在能用cPickle的时候最好用它,比如像小林一样:

try:
import cPickle as pickle
except ImportError:
import pickle

  pickle模块可以处理的对象类型有None, 数字和字符串, 只包含可序列化对象的元组,列表,字典,以及用户自定义的类的实例等。

  pickle模块的format_version成员变量可以查看当前pickle的格式版本,而compatible_formats变量可以显示当前pickle兼容哪些之前的版本。

■  基本用法

  pickle的基本用法很简单,基本上就是四个方法,dumps,dump 以及 loads,load。

  dumps(object[,protocol])  把一个对象序列化,返回一个包含序列化信息的字符串

  dump(object,file[,protocol])  把一个对象序列化后存储到文件file中。

  以上两者的protocol参数用来指定数据的输出格式,0是默认值,指文本格式且兼容之前的所有python版本,1指二进制格式且向前兼容所有python版本;2,3也各有所指,不详述了。当object不支持序列化时会在调用dump时引发pickle.PicklingError异常。

  loads(string)  从一个包含序列化信息的字符串中解析出一个对象并返回

  load(file)  从一个文件中加载解析出一个对象并返回

  以上两个方法无需指定协议,方法会自动判断当前序列化内容的协议。如果文件含有无法解码的数据(原因包括格式不对,数据遭到损坏等等可能)则会引发pickle.UnpicklingError异常。另外在load方法中如果检测到文件的结尾则会引发EOFError异常,可以利用这个异常来判断循环遍历load时结束的时机。

■  更多细节

  ●  关于在序列化存入文件时的dump顺序和load顺序的关系:

    当把多个对象dump到一个文件里,再load的时候会发生什么?其实,pickle在load的时候会自动识别文件中对象之间的分隔然后一个个地load出来。比如我先后把A和B pickle.dump进一个文件中,然后再从这个文件中先后pickle.load出C和D。这些对象间的对应关系就是A和C相等,B和D相等。

  ●  上面提到了关于不支持序列化的对象。比如文件对象就是一个不支持序列化的对象。想想也是啊,把文件对象固化到另一个文件中,确实没什么必要。。

  ●  经过pickle的dump再load出来的对象,在内容上和原对象相等,但是并不是同一个对象。新出来的对象应该是原对象的一个副本。比如把A dump进一个文件然后load出B。此时A==B是True但是A is B是False。并且,每一次pickle.load其实际上是从文件中load出一个值,然后创建一个新对象来引用这个值,所以无论之前dump进文件的对象的引用情况是怎么样的,load出来的对象之间都是互相独立的

a = [1,2,3]
b = a
print a is b #True
fi = open("file.pk","w")
pickle.dump(a,fi)
pickle.dump(b,fi)
fi.close() fi = open("file.pk","r")
c = pickle.load(fi)
d = pickle.load(fi)
fi.close()
print c is d #False

  如果想要让c和d也都是引用同一个内容的话,那就需要在dump的时候就把这种关系确定下来。也就是说要把两个对象引用的内容是相同的这个情况用“值”的形式也固化到文件中去,一个常见的做法就是把原先分两个dump换成dump一个元组:

a = [1,2,3]
b = a
##文件开关都省略了##
pickle.dump((a,b),fi) c,d = pickle.load(fi) print c is d #True

  这样做的话就可以使得dump操作把这种引用关系也dump进文件里面了。但是这样做并不好,一点都不好懂搞一个元组出来有什么意义。所以pickle还提供了Pickler和Unpickler两个类。这两个类可以在还原的时候还原出原来的对象之间的关系。构造方法是Pickler(file[,protocol])。比如:

a = [1,2,3]
b = a
fi = open("file","w")
pickler = pickle.Pickler(fi)
pickler.dump(a)
pickler.dump(b)
fi.close() fi = open("file","r")
unpickler = pickle.Unpickler(fi)
c = unpickler.load()
d = unpickler.load()
print c is d #True
fi.close()

  究其原理,其实是Pickler类里面有一个字典,会记住每一个dump进来的对象的id(而不仅仅是它的值),当接下来dump进来的对象有相同的id时它就为这个对象在文件中创建一个引用就好了而不是一个有完整内容的副本。这样的一个文件,在load的时候自然也就会保留出两个原来的对象引用同一片内容的特点了。注意:用Pickler类dump出来的文件一定要用Unpickler类来解析,普通的pickle.load可能会出现错误。

  Pickler类中还有一个pickler.clear_memo()方法,就是用来清空Pickler类中的那个字典,使得新dump进来的对象即使拥有和之前dump进的对象相同的引用也会创建一个新的副本。

  ●  关于自定义类实例的pickle

  pickle模块可以处理的对象类型有None, 数字和字符串, 只包含可序列化对象的元组,列表,字典,以及用户自定义的类的实例等。在处理自定义类的实例的时候要特别注意,对于这种实例的序列化,pickle基本上只会记录这个实例各种属性的值(通常是__dict__属性中的),而不记录类和成员方法的代码。所以当在反序列化(或者把这个行为称之为重建实例)实例的时候,如果在当前的Unpickler所在环境下找不到相关的类或方法的定义,或者相关定义不能满足现在这个实例的一些操作的时候是会报错的。另外需要注意的是,重建实例时并不会调用这个实例所属类的__init__方法,所以“重建”并不是从零开始的重建,而是把记录在文件里的那些数据填入到现有的一个框架里面的过程。比如两个放在同一目录下的脚本,第一个脚本:

class Test(object):
def __init__(self,msg):
self.msg = msg def printmymessage(self):
print self.msg test = Test("Hello")
test.msg = "konnnitiha"
fi = open("test.pk","w")
pickler = pickle.Pickler(fi)
pickler.dump(test)
fi.close()

第二个脚本:

class Test(object):
def __init__(self,msg):
self.msg = msg def printmymessage(self):
print "new class "+self.msg
fi = open("test.pk","r")
u = pickle.Unpickler(fi)
myins = u.load()
myins.printmymessage() #打印出来的是new class konnnitiha
fi.close()

  这样的一个输出说明了两个问题。1.被dump进文件的对象是带有其最新的属性值的,所以输出的是konnitiha而不是Hello。2.重建实例的时候用的类和方法的定义是看Unpickler所在环境决定的,比如这里的打印信息有new class,反过来说,如果脚本2里没有定义Test类或者printmymessage方法的话就会报错了。

  另外要注意,要dump的类的类定义必须出现在模块的最顶层,这意味着它们不能是嵌套的类(在其它类或函数中定义的类)。

  ●  再论自定义类实例的pickle,__getstate__和__setstate__方法

  首先要说的是类的特殊方法,__getstate__和__setstate__。pickle一个类的实例时其实是把这个类的__getstate__方法返回的值存了起来,在默认情况下这个方法返回的就是类的__dict__。而unpickle就是把从文件解析出来的东西传递给__setstate__方法,让这个方法做一些处理(默认就是把得到的东西传给新实例的__dict__),这相当于是重建的过程。

  通过对一个类的这两个特殊方法的重载,可以灵活地控制经过pickle和unpickle后实例,使得前后两个实例带有的数据可以不一样(在默认情况下两者肯定是完全相同的)。比如在getstate里面只返回一部分的属性等。下面就是一个例子:(https://www.oschina.net/question/253614_115412)

class Slate:
'''存储一个字符串和一个变更log,当Pickle时会忘记它的值''' def __init__(self, value):
self.value = value
self.last_change = time.asctime()
self.history = {} def change(self, new_value):
# 改变值,提交最后的值到历史记录
self.history[self.last_change] = self.value
self.value = new_value
self.last_change = time.asctime() def print_changes(self):
print 'Changelog for Slate object:'
for k, v in self.history.items():
print '%st %s' % (k, v) def __getstate__(self):
# 故意不返回self.value 或 self.last_change.
# 当unpickle,我们希望有一块空白的"slate"
return self.history def __setstate__(self, state):
# 让 self.history = state 和 last_change 和 value被定义
self.history = state
self.value, self.last_change = None, None

  这个例子可以实现每次把Slate类的实例pickle到磁盘上时,让pickle只记录它的修改历史记录而不记录当前值和最后修改时间,当在另一个地方unpickle它,重建实例的时候根据__setstate__方法的指定,历史记录被写进新的实例中,但是值和最后修改时间被指定为None也就是空白状态。

  ●  通过pickle得到的序列化数据是python专用的,只有python才能解析,并不像xml,json这种数据格式可以通用到很多体系中。