Python Cookbook-7.4 对类和实例使用 cPickle 模块
任务
想通过 cPickle 模块来存取类和实例对象。
解决方案
在用 cPickle 处理你的类和实例时,有很多需要注意的地方。比如,下面这段代码看上去似乎工作得很好:
import cPickle
class ForExample(object):def __init__(self,*stuff):self.stuff = stuff
anInstance = ForExample('one',2,3)
saved = cPickle.dumps(anInstance)
reloaded = cPickle.loads(saved)
assert anInstance.stuff == reloaded.stuff
然而,有时仍会出一些问题:
anotherInstance = ForExample(1,2,open('three','w'))
wontWork = cPickle.dumps(anotherInstance)
这个片段子句引发了一个 TypeError:“can’t pickle file objects”异常,这是因为anotherInstance 包含了一个文件对象,而文件对象是无法用 pickle 处理的。如果你试图pickle 另一个含有文件对象的容器,你会得到相同的异常。然而,在某些情况下,可以为此做点什么:
class PrettyClever(object):def __init__(self,*stuff):self.stuff = stuffdef __getstate__(self):def normalize(x):if isinstance(x,file):return 1,(x.name,x.mode,x.tell())return 0,xreturn [normalize(x) for x in self.stuff]def __setstate__(self,stuff):def reconstruct(x):if x[0] == 0:return x[1]name,mode,offs = x[1]openfile = open(name,mode)openfile.seek(offs)return openfileself.stuff = tuple([reconstruct(x) for x in stuff])
通过定义你的类的__getstate__和__setstate__特殊方法,你能够以某种更好的方式控制你的类实例,并影响到它把什么作为自己的状态。只要你能够用一种可被pickle处理的方式定义状态,并以某种对你的应用而言已经够用的方式从处理过的状态重新构建实例,你就能够为你的实例实现 pickle 操作及反操作。
讨论
cPickle 通过名字处理类和函数对象(比如通过它们的模块名和它们在模块内部的名字)。因此,你只能处理模块级别(不在其他类或函数中)的类。只有各个模块是能够被导入的,我们才能重新载入这些类对象。而且只有在实例属于这些类的前提下,它们才能够被保存和重新载入。除此之外,这些实例的状态也必须是可被pickle 处理的。默认情况下,实例的状态就是实例的__dict __的内容,再加上一些继承来的内建类型如果有继承的话。比如,一个从list派生来的新风格类的实例包含列表的子项,这些子项也是它的状态的一个部分。cPickle同样能够处理定义或继承了名为slots(因此在那些预定义的槽中会含有实例的状态,而不是在ict中)的类属性的新风格类的实例。总的来说,对我们的应用而言,cPickle的默认方式通常就可以满足需要了。
然而有时候,你的实例会含有一些不能被 pickle 处理的属性或子项,并将这些属性或子项作为自身的状态(前一段提到过,cPickle 默认会将这些属性或子项作为状态)。在本节中,作为示例,我展示了一个类的实例,它含有一个可以是任何类型的stuff,stuff有可能是打开的文件对象。为了处理这个问题,你的类可以定义特殊方法__getstate__。如果你的对象的类定义或继承了此方法,cPickle 会对你的对象调用它,而不是直接到对象的__dict__中刨根问底(或者__slots__中,或内建类型的基类中)。正常情况下,如果你定义了__getstate__方法,你也会定义__setstate__方法,就像解决方案中的做法。getstate__可返回所有能被 pickle 处理的对象,以及被 pickle 处理过的对象,当以后进行反 pickle 操作的时候,那些对象还会被当做参数传递给__setstate。在本节的解决方案中,getstate__返回了一个列表,很像实例的默认状态(属性self.stuff),只是每个子项都被转变为一个含有两个子项的元组。如果元组子项的第一项被设置为 0,则表示要逐字地接纳第二个子项,如果为1,则表示第二个子项会被用来构建一个打开的文件(当然,重新构建可能会失败或者无效。并没有一种通用的方法能够记录一个打开的文件的状态,这就是为什么cPickle 连试都不想试一下。但在我们的应用环境下,我们可以假设给出的方法能够工作)。当我们进行反pickle 操作并重新载入实例时,cPickle 会对列表中的子项调用__setstate,__setstate__能够用嵌套的 reconstruct 函数正确地处理每一对子项,并构建出self.stuff。对于那些在正常情况下无法用 pickle 处理的对象的状态,这种模式具有一定通用性,只是你要记得用不同的数字来标示你想支持的各种不同的“无法逐字接纳”的类型。
在某种特殊情况下,可以只定义__getstate__,而不定义__setstate__:getstate__必须返回一个字典,而当我们进行反 pickle 操作并恢复实例时,就可以像使用实例的dict 一样直接使用这个字典。在重新载入的时候,不能运行你自己的代码可能是一个不便之处,但可以使用__getstate,它非常方便,用它的目的并不是为了保存状态,只是为了优化。一种可以用它优化的典型情况是,假设你的实例缓存了一些结果,当使用缓存时,如果访问到不存在的数据会引发重新计算,而你认为最好不要将这些缓存结果作为状态保存。在这种情况下,应当定义__getstate__来返回一个字典,作为实例的__dict__的不可或缺的一个子集(见4.13 节,用一种简单而方便的方法“获取字典的子集”)。
除了 pickling 支持,定义__getstate__(以及通常也会定义的__setstate__)也会带给你更多的其他好处:如果一个类提供了这些方法而没有提供特殊方法__copy__或__deepcopy__,则这些方法不仅可以用于序列化也可以用于拷贝,包括浅拷贝和深拷贝。如果__getstate__返回的状态数据是被深度复制的,那么整个对象就被深度复制了,不过,除了这点区别,通过__getstate__来实现的浅拷贝和深拷贝非常相似。见4.1节中更多的关于类如何控制它的实例的复制方式的内容。
无论是通过默认的 pickling和 upickling方式,还是使用自身的__getstate__和__setstate__,当实例从被 pickle 过的状态恢复时,它的特殊方法__init__都不会被调用。如果对你来说最方便的方法是通过调用带有参数的__init__方法来重建实例,你可能会需要定义一个特殊方法__getinitargs__,而不是__getstate__。在这种情况下,cPickle将不用参数调用此方法:在重新载入的过程中,这个方法必须返回一个可以被pickle处理的元组,cPickle用此元组的子项作为__init__的参数并调用之。getinitargs,就像__getstate__和__setstate__,也可用于复制。
Library Reference 中关于 pickle 和 copy_reg 模块的内容涉及了几乎所有关于 pickling和 unpickling的细节,甚至还包括了棘手的安全性问题,特别是当你试图从一个不可信任的源 unpickle 数据的时候。(注意:别这么做——如果你坚持这样做,Python 无法保护你)不过,我在这里谈到的一些技术对于绝大多数实践工作已经够用了,只要别涉及太多的安全性问题(如果确实有安全性问题,那么最具操作性的建议是:不要用 pickle)。