python pickle模块
本文内容纲要:
持久性就是指保持对象,甚至在多次执行同一程序之间也保持对象。通过本文,您会对Python对象的各种持久性机制(从关系数据库到Python的pickle以及其它机制)有一个总体认识。另外,还会让您更深一步地了解Python的对象序列化能力。
什么是持久性?
持久性的基本思想很简单。假定有一个Python程序,它可能是一个管理日常待办事项的程序,您希望在多次执行这个程序之间可以保存应用程序对象(待办事项)。换句话说,您希望将对象存储在磁盘上,便于以后检索。这就是持久性。要达到这个目的,有几种方法,每一种方法都有其优缺点。
例如,可以将对象数据存储在某种格式的文本文件中,譬如CSV文件。或者可以用关系数据库,譬如Gadfly、MySQL、PostgreSQL或者DB2。这些文件格式和数据库都非常优秀,对于所有这些存储机制,Python都有健壮的接口。
这些存储机制都有一个共同点:存储的数据是独立于对这些数据进行操作的对象和程序。这样做的好处是,数据可以作为共享的资源,供其它应用程序使用。缺点是,用这种方式,可以允许其它程序访问对象的数据,这违背了面向对象的封装性原则—即对象的数据只能通过这个对象自身的公共(public)接口来访问。
另外,对于某些应用程序,关系数据库方法可能不是很理想。尤其是,关系数据库不理解对象。相反,关系数据库会强行使用自己的类型系统和关系数据模型(表),每张表包含一组元组(行),每行包含具有固定数目的静态类型字段(列)。如果应用程序的对象模型不能够方便地转换到关系模型,那么在将对象映射到元组以及将元组映射回对象方面,会碰到一定难度。这种困难常被称为阻碍性不匹配(impedence-mismatch)问题。
对象持久性
如果希望透明地存储Python对象,而不丢失其身份和类型等信息,则需要某种形式的对象序列化:它是一个将任意复杂的对象转成对象的文本或二进制表示的过程。同样,必须能够将对象经过序列化后的形式恢复到原有的对象。在Python中,这种序列化过程称为pickle,可以将对象pickle成字符串、磁盘上的文件或者任何类似于文件的对象,也可以将这些字符串、文件或任何类似于文件的对象unpickle成原来的对象。我们将在本文后面详细讨论pickle。
假定您喜欢将任何事物都保存成对象,而且希望避免将对象转换成某种基于非对象存储的开销;那么pickle文件可以提供这些好处,但有时可能需要比这种简单的pickle文件更健壮以及更具有可伸缩性的事物。例如,只用pickle不能解决命名和查找pickle文件这样的问题,另外,它也不能支持并发地访问持久性对象。如果需要这些方面的功能,则要求助类似于ZODB(针对Python的Z对象数据库)这类数据库。ZODB是一个健壮的、多用户的和面向对象的数据库系统,它能够存储和管理任意复杂的Python对象,并支持事务操作和并发控制。(请参阅参考资料,以下载ZODB。)令人足够感兴趣的是,甚至ZODB也依靠Python的本机序列化能力,而且要有效地使用ZODB,必须充分了解pickle。
另一种令人感兴趣的解决持久性问题的方法是Prevayler,它最初是用Java实现的(有关Prevaylor方面的developerWorks文章,请参阅参考资料)。最近,一群Python程序员将Prevayler移植到了Python上,另起名为PyPerSyst,由SourceForge托管(有关至PyPerSyst项目的链接,请参阅参考资料)。Prevayler/PyPerSyst概念也是建立在Java和Python语言的本机序列化能力之上。PyPerSyst将整个对象系统保存在内存中,并通过不时地将系统快照pickle到磁盘以及维护一个命令日志(通过此日志可以重新应用最新的快照)来提供灾难恢复。所以,尽管使用PyPerSyst的应用程序受到可用内存的限制,但好处是本机对象系统可以完全装入到内存中,因而速度极快,而且实现起来要比如ZODB这样的数据库简单,ZODB允许对象的数目比同时在能内存中所保持的对象要多。
既然我们已经简要讨论了存储持久对象的各种方法,那么现在该详细探讨pickle过程了。虽然我们主要感兴趣的是探索以各种方式来保存Python对象,而不必将其转换成某种其它格式,但我们仍然还有一些需要关注的地方,譬如:如何有效地pickle和unpickle简单对象以及复杂对象,包括定制类的实例;如何维护对象的引用,包括循环引用和递归引用;以及如何处理类定义发生的变化,从而使用以前经过pickle的实例时不会发生问题。我们将在随后关于Python的pickle能力探讨中涉及所有这些问题。
一些经过pickle的Python
pickle模块及其同类模块cPickle向Python提供了pickle支持。后者是用C编码的,它具有更好的性能,对于大多数应用程序,推荐使用该模块。我们将继续讨论pickle,但本文的示例实际是利用了cPickle。由于其中大多数示例要用Pythonshell来显示,所以先展示一下如何导入cPickle,并可以作为pickle来引用它:
importcPickleaspickle
现在已经导入了该模块,接下来让我们看一下pickle接口。pickle模块提供了以下函数对:dumps(object)返回一个字符串,它包含一个pickle格式的对象;loads(string)返回包含在pickle字符串中的对象;dump(object,file)将对象写到文件,这个文件可以是实际的物理文件,但也可以是任何类似于文件的对象,这个对象具有write()方法,可以接受单个的字符串参数;load(file)返回包含在pickle文件中的对象。
缺省情况下,dumps()和dump()使用可打印的ASCII表示来创建pickle。两者都有一个final参数(可选),如果为True,则该参数指定用更快以及更小的二进制表示来创建pickle。loads()和load()函数自动检测pickle是二进制格式还是文本格式。
清单1显示了一个交互式会话,这里使用了刚才所描述的dumps()和loads()函数:
清单1.dumps()和loads()的演示
[python]viewplaincopyprint?
importcPickleaspickle
t1=('thisisastring',42,[1,2,3],None)
t1
- ('thisisastring',42,[1,2,3],None)
p1=pickle.dumps(t1)
p1
- "(S'thisisastring'/nI42/n(lp1/nI1/naI2/naI3/naNtp2/n."
printp1
- (S'thisisastring'
- I42
- (lp1
- I1
- aI2
- aI3
- aNtp2
- .
t2=pickle.loads(p1)
t2
- ('thisisastring',42,[1,2,3],None)
p2=pickle.dumps(t1,True)
p2
- '(U/x10thisisastringK*]q/x01(K/x01K/x02K/x03eNtq/x02.'
t3=pickle.loads(p2)
t3
- ('thisisastring',42,[1,2,3],None)
importcPickleaspickle>>>t1=('thisisastring',42,[1,2,3],None)>>>t1('thisisastring',42,[1,2,3],None)>>>p1=pickle.dumps(t1)>>>p1"(S'thisisastring'/nI42/n(lp1/nI1/naI2/naI3/naNtp2/n.">>>printp1(S'thisisastring'I42(lp1I1aI2aI3aNtp2.>>>t2=pickle.loads(p1)>>>t2('thisisastring',42,[1,2,3],None)>>>p2=pickle.dumps(t1,True)>>>p2'(U/x10thisisastringK*]q/x01(K/x01K/x02K/x03eNtq/x02.'>>>t3=pickle.loads(p2)>>>t3('thisisastring',42,[1,2,3],None)
注:该文本pickle格式很简单,这里就不解释了。事实上,在pickle模块中记录了所有使用的约定。我们还应该指出,在我们的示例中使用的都是简单对象,因此使用二进制pickle格式不会在节省空间上显示出太大的效率。然而,在实际使用复杂对象的系统中,您会看到,使用二进制格式可以在大小和速度方面带来显著的改进。
接下来,我们看一些示例,这些示例用到了dump()和load(),它们使用文件和类似文件的对象。这些函数的操作非常类似于我们刚才所看到的dumps()和loads(),区别在于它们还有另一种能力—dump()函数能一个接着一个地将几个对象转储到同一个文件。随后调用load()来以同样的顺序检索这些对象。清单2显示了这种能力的实际应用:
清单2.dump()和load()示例
[python]viewplaincopyprint?
a1='apple'
b1={1:'One',2:'Two',3:'Three'}
c1=['fee','fie','foe','fum']
f1=file('temp.pkl','wb')
pickle.dump(a1,f1,True)
pickle.dump(b1,f1,True)
pickle.dump(c1,f1,True)
f1.close()
f2=file('temp.pkl','rb')
a2=pickle.load(f2)
a2
- 'apple'
b2=pickle.load(f2)
b2
- {1:'One',2:'Two',3:'Three'}
c2=pickle.load(f2)
c2
- ['fee','fie','foe','fum']
f2.close()
a1='apple'>>>b1={1:'One',2:'Two',3:'Three'}>>>c1=['fee','fie','foe','fum']>>>f1=file('temp.pkl','wb')>>>pickle.dump(a1,f1,True)>>>pickle.dump(b1,f1,True)>>>pickle.dump(c1,f1,True)>>>f1.close()>>>f2=file('temp.pkl','rb')>>>a2=pickle.load(f2)>>>a2'apple'>>>b2=pickle.load(f2)>>>b2{1:'One',2:'Two',3:'Three'}>>>c2=pickle.load(f2)>>>c2['fee','fie','foe','fum']>>>f2.close()
Pickle的威力
到目前为止,我们讲述了关于pickle方面的基本知识。在这一节,将讨论一些高级问题,当您开始pickle复杂对象时,会遇到这些问题,其中包括定制类的实例。幸运的是,Python可以很容易地处理这种情形。
可移植性
从空间和时间上说,Pickle是可移植的。换句话说,pickle文件格式独立于机器的体系结构,这意味着,例如,可以在Linux下创建一个pickle,然后将它发送到在Windows或MacOS下运行的Python程序。并且,当升级到更新版本的Python时,不必担心可能要废弃已有的pickle。Python开发人员已经保证pickle格式将可以向后兼容Python各个版本。事实上,在pickle模块中提供了有关目前以及所支持的格式方面的详细信息.
清单3.检索所支持的格式
[python]viewplaincopyprint?
pickle.format_version
- '1.3'
pickle.compatible_formats
- ['1.0','1.1','1.2']
pickle.format_version'1.3'>>>pickle.compatible_formats['1.0','1.1','1.2']
多个引用,同一对象
在Python中,变量是对象的引用。同时,也可以用多个变量引用同一个对象。经证明,Python在用经过pickle的对象维护这种行为方面丝毫没有困难,如清单4所示:
清单4.对象引用的维护
[python]viewplaincopyprint?
a=[1,2,3]
b=a
a
- [1,2,3]
b
- [1,2,3]
a.append(4)
a
- [1,2,3,4]
b
- [1,2,3,4]
c=pickle.dumps((a,b))
d,e=pickle.loads(c)
d
- [1,2,3,4]
e
- [1,2,3,4]
d.append(5)
d
- [1,2,3,4,5]
e
- [1,2,3,4,5]
a=[1,2,3]>>>b=a>>>a[1,2,3]>>>b[1,2,3]>>>a.append(4)>>>a[1,2,3,4]>>>b[1,2,3,4]>>>c=pickle.dumps((a,b))>>>d,e=pickle.loads(c)>>>d[1,2,3,4]>>>e[1,2,3,4]>>>d.append(5)>>>d[1,2,3,4,5]>>>e[1,2,3,4,5]
循环引用和递归引用
可以将刚才演示过的对象引用支持扩展到循环引用(两个对象各自包含对对方的引用)和递归引用(一个对象包含对其自身的引用)。下面两个清单着重显示这种能力。我们先看一下递归引用:
清单5.递归引用
[python]viewplaincopyprint?
l=[1,2,3]
l.append(l)
l
- [1,2,3,[...]]
l[3]
- [1,2,3,[...]]
l[3][3]
- [1,2,3,[...]]
p=pickle.dumps(l)
l2=pickle.loads(p)
l2
- [1,2,3,[...]]
l2[3]
- [1,2,3,[...]]
l2[3][3]
- [1,2,3,[...]]
l=[1,2,3]>>>l.append(l)>>>l[1,2,3,[...]]>>>l[3][1,2,3,[...]]>>>l[3][3][1,2,3,[...]]>>>p=pickle.dumps(l)>>>l2=pickle.loads(p)>>>l2[1,2,3,[...]]>>>l2[3][1,2,3,[...]]>>>l2[3][3][1,2,3,[...]]
现在,看一个循环引用的示例:
清单6.循环引用
[python]viewplaincopyprint?
a=[1,2]
b=[3,4]
a.append(b)
a
- [1,2,[3,4]]
b.append(a)
a
- [1,2,[3,4,[...]]]
b
- [3,4,[1,2,[...]]]
a[2]
- [3,4,[1,2,[...]]]
b[2]
- [1,2,[3,4,[...]]]
a[2]isb
- 1
b[2]isa
- 1
f=file('temp.pkl','w')
pickle.dump((a,b),f)
f.close()
f=file('temp.pkl','r')
c,d=pickle.load(f)
f.close()
c
- [1,2,[3,4,[...]]]
d
- [3,4,[1,2,[...]]]
c[2]
- [3,4,[1,2,[...]]]
d[2]
- [1,2,[3,4,[...]]]
c[2]isd
- 1
d[2]isc
- 1
a=[1,2]>>>b=[3,4]>>>a.append(b)>>>a[1,2,[3,4]]>>>b.append(a)>>>a[1,2,[3,4,[...]]]>>>b[3,4,[1,2,[...]]]>>>a[2][3,4,[1,2,[...]]]>>>b[2][1,2,[3,4,[...]]]>>>a[2]isb1>>>b[2]isa1>>>f=file('temp.pkl','w')>>>pickle.dump((a,b),f)>>>f.close()>>>f=file('temp.pkl','r')>>>c,d=pickle.load(f)>>>f.close()>>>c[1,2,[3,4,[...]]]>>>d[3,4,[1,2,[...]]]>>>c[2][3,4,[1,2,[...]]]>>>d[2][1,2,[3,4,[...]]]>>>c[2]isd1>>>d[2]isc1
注意,如果分别pickle每个对象,而不是在一个元组中一起pickle所有对象,会得到略微不同(但很重要)的结果,如清单7所示:
清单7.分别picklevs.在一个元组中一起pickle
[python]viewplaincopyprint?
f=file('temp.pkl','w')
pickle.dump(a,f)
pickle.dump(b,f)
f.close()
f=file('temp.pkl','r')
c=pickle.load(f)
d=pickle.load(f)
f.close()
c
- [1,2,[3,4,[...]]]
d
- [3,4,[1,2,[...]]]
c[2]
- [3,4,[1,2,[...]]]
d[2]
- [1,2,[3,4,[...]]]
c[2]isd
- 0
d[2]isc
- 0
f=file('temp.pkl','w')>>>pickle.dump(a,f)>>>pickle.dump(b,f)>>>f.close()>>>f=file('temp.pkl','r')>>>c=pickle.load(f)>>>d=pickle.load(f)>>>f.close()>>>c[1,2,[3,4,[...]]]>>>d[3,4,[1,2,[...]]]>>>c[2][3,4,[1,2,[...]]]>>>d[2][1,2,[3,4,[...]]]>>>c[2]isd0>>>d[2]isc0
相等,但并不总是相同
正如在上一个示例所暗示的,只有在这些对象引用内存中同一个对象时,它们才是相同的。在pickle情形中,每个对象被恢复到一个与原来对象相等的对象,但不是同一个对象。换句话说,每个pickle都是原来对象的一个副本:
清单8.作为原来对象副本的被恢复的对象
[python]viewplaincopyprint?
j=[1,2,3]
k=j
kisj
- 1
x=pickle.dumps(k)
y=pickle.loads(x)
y
- [1,2,3]
y==k
- 1
yisk
- 0
yisj
- 0
kisj
- 1
j=[1,2,3]>>>k=j>>>kisj1>>>x=pickle.dumps(k)>>>y=pickle.loads(x)>>>y[1,2,3]>>>y==k1>>>yisk0>>>yisj0>>>kisj1
同时,我们看到Python能够维护对象之间的引用,这些对象是作为一个单元进行pickle的。然而,我们还看到分别调用dump()会使Python无法维护对在该单元外部进行pickle的对象的引用。相反,Python复制了被引用对象,并将副本和被pickle的对象存储在一起。对于pickle和恢复单个对象层次结构的应用程序,这是没有问题的。但要意识到还有其它情形。
值得指出的是,有一个选项确实允许分别pickle对象,并维护相互之间的引用,只要这些对象都是pickle到同一文件即可。pickle和cPickle模块提供了一个Pickler(与此相对应是Unpickler),它能够跟踪已经被pickle的对象。通过使用这个Pickler,将会通过引用而不是通过值来pickle共享和循环引用:
清单9.维护分别pickle的对象间的引用
[python]viewplaincopyprint?
f=file('temp.pkl','w')
pickler=pickle.Pickler(f)
pickler.dump(a)
- <cPickle.Picklerobjectat0x89b0bb8>
pickler.dump(b)
- <cPickle.Picklerobjectat0x89b0bb8>
f.close()
f=file('temp.pkl','r')
unpickler=pickle.Unpickler(f)
c=unpickler.load()
d=unpickler.load()
c[2]
- [3,4,[1,2,[...]]]
d[2]
- [1,2,[3,4,[...]]]
c[2]isd
- 1
d[2]isc
- 1
f=file('temp.pkl','w')>>>pickler=pickle.Pickler(f)>>>pickler.dump(a)<cPickle.Picklerobjectat0x89b0bb8>>>>pickler.dump(b)<cPickle.Picklerobjectat0x89b0bb8>>>>f.close()>>>f=file('temp.pkl','r')>>>unpickler=pickle.Unpickler(f)>>>c=unpickler.load()>>>d=unpickler.load()>>>c[2][3,4,[1,2,[...]]]>>>d[2][1,2,[3,4,[...]]]>>>c[2]isd1>>>d[2]isc1
不可pickle的对象
一些对象类型是不可pickle的。例如,Python不能pickle文件对象(或者任何带有对文件对象引用的对象),因为Python在unpickle时不能保证它可以重建该文件的状态(另一个示例比较难懂,在这类文章中不值得提出来)。试图pickle文件对象会导致以下错误:
清单10.试图pickle文件对象的结果
[python]viewplaincopyprint?
f=file('temp.pkl','w')
p=pickle.dumps(f)
- Traceback(mostrecentcalllast):
- File"",line1,in?
- File"/usr/lib/python2.2/copy_reg.py",line57,in_reduce
- raiseTypeError,"can'tpickle%sobjects"%base.__name__
- TypeError:can'tpicklefileobjects
f=file('temp.pkl','w')>>>p=pickle.dumps(f)Traceback(mostrecentcalllast):File"",line1,in?File"/usr/lib/python2.2/copy_reg.py",line57,in_reduceraiseTypeError,"can'tpickle%sobjects"%base.__name__TypeError:can'tpicklefileobjects
类实例
与pickle简单对象类型相比,pickle类实例要多加留意。这主要由于Python会pickle实例数据(通常是_dict_属性)和类的名称,而不会pickle类的代码。当Pythonunpickle类的实例时,它会试图使用在pickle该实例时的确切的类名称和模块名称(包括任何包的路径前缀)导入包含该类定义的模块。另外要注意,类定义必须出现在模块的最顶层,这意味着它们不能是嵌套的类(在其它类或函数中定义的类)。
当unpickle类的实例时,通常不会再调用它们的_init_()方法。相反,Python创建一个通用类实例,并应用已进行过pickle的实例属性,同时设置该实例的_class_属性,使其指向原来的类。
对Python2.2中引入的新型类进行unpickle的机制与原来的略有不同。虽然处理的结果实际上与对旧型类处理的结果相同,但Python使用copy_reg模块的_reconstructor()函数来恢复新型类的实例。
如果希望对新型或旧型类的实例修改缺省的pickle行为,则可以定义特殊的类的方法_getstate_()和_setstate_(),在保存和恢复类实例的状态信息期间,Python会调用这些方法。在以下几节中,我们会看到一些示例利用了这些特殊的方法。
现在,我们看一个简单的类实例。首先,创建一个persist.py的Python模块,它包含以下新型类的定义:
清单11.新型类的定义
[python]viewplaincopyprint?
- classFoo(object):
- def__init__(self,value):
- self.value=value
classFoo(object):def__init__(self,value):self.value=value
现在可以pickleFoo实例,并看一下它的表示:
清单12.pickleFoo实例
[python]viewplaincopyprint?
importcPickleaspickle
fromOrbtech.examples.persistimportFoo
foo=Foo('WhatisaFoo?')
p=pickle.dumps(foo)
printp
- ccopy_reg
- _reconstructor
- p1
- (cOrbtech.examples.persist
- Foo
- p2
- c__builtin__
- object
- p3
- NtRp4
- (dp5
- S'value'
- p6
- S'WhatisaFoo?'
- sb.
importcPickleaspickle>>>fromOrbtech.examples.persistimportFoo>>>foo=Foo('WhatisaFoo?')>>>p=pickle.dumps(foo)>>>printpccopy_reg_reconstructorp1(cOrbtech.examples.persistFoop2c__builtin__objectp3NtRp4(dp5S'value'p6S'WhatisaFoo?'sb.
可以看到这个类的名称Foo和全限定的模块名称Orbtech.examples.persist都存储在pickle中。如果将这个实例pickle成一个文件,稍后再unpickle它或在另一台机器上unpickle,则Python会试图导入Orbtech.examples.persist模块,如果不能导入,则会抛出异常。如果重命名该类和该模块或者将该模块移到另一个目录,则也会发生类似的错误。
这里有一个Python发出错误消息的示例,当我们重命名Foo类,然后试图装入先前进行过pickle的Foo实例时会发生该错误:
清单13.试图装入一个被重命名的Foo类的经过pickle的实例
[python]viewplaincopyprint?
importcPickleaspickle
f=file('temp.pkl','r')
foo=pickle.load(f)
- Traceback(mostrecentcalllast):
- File"",line1,in?
- AttributeError:'module'objecthasnoattribute'Foo'
importcPickleaspickle>>>f=file('temp.pkl','r')>>>foo=pickle.load(f)Traceback(mostrecentcalllast):File"",line1,in?AttributeError:'module'objecthasnoattribute'Foo'
在重命名persist.py模块之后,也会发生类似的错误:
清单14.试图装入一个被重命名的persist.py模块的经过pickle的实例
[python]viewplaincopyprint?
importcPickleaspickle
f=file('temp.pkl','r')
foo=pickle.load(f)
- Traceback(mostrecentcalllast):
- File"",line1,in?
- ImportError:Nomodulenamedpersist
importcPickleaspickle>>>f=file('temp.pkl','r')>>>foo=pickle.load(f)Traceback(mostrecentcalllast):File"",line1,in?ImportError:Nomodulenamedpersist
我们会在下面模式改进这一节提供一些技术来管理这类更改,而不会破坏现有的pickle。
特殊的状态方法
前面提到对一些对象类型(譬如,文件对象)不能进行pickle。处理这种不能pickle的对象的实例属性时可以使用特殊的方法(_getstate_()和_setstate_())来修改类实例的状态。这里有一个Foo类的示例,我们已经对它进行了修改以处理文件对象属性:
清单15.处理不能pickle的实例属性
[python]viewplaincopyprint?
- classFoo(object):
- def__init__(self,value,filename):
- self.value=value
- self.logfile=file(filename,'w')
- def__getstate__(self):
- """Returnstatevaluestobepickled."""
- f=self.logfile
- return(self.value,f.name,f.tell())
- def__setstate__(self,state):
- """Restorestatefromtheunpickledstatevalues."""
- self.value,name,position=state
- f=file(name,'w')
- f.seek(position)
- self.logfile=f
classFoo(object):def__init__(self,value,filename):self.value=valueself.logfile=file(filename,'w')def__getstate__(self):"""Returnstatevaluestobepickled."""f=self.logfilereturn(self.value,f.name,f.tell())def__setstate__(self,state):"""Restorestatefromtheunpickledstatevalues."""self.value,name,position=statef=file(name,'w')f.seek(position)self.logfile=f
pickleFoo的实例时,Python将只pickle当它调用该实例的_getstate_()方法时返回给它的值。类似的,在unpickle时,Python将提供经过unpickle的值作为参数传递给实例的_setstate_()方法。在_setstate_()方法内,可以根据经过pickle的名称和位置信息来重建文件对象,并将该文件对象分配给这个实例的logfile属性。
模式改进
随着时间的推移,您会发现自己必须要更改类的定义。如果已经对某个类实例进行了pickle,而现在又需要更改这个类,则您可能要检索和更新那些实例,以便它们能在新的类定义下继续正常工作。而我们已经看到在对类或模块进行某些更改时,会出现一些错误。幸运的是,pickle和unpickle过程提供了一些hook,我们可以用它们来支持这种模式改进的需要。
在这一节,我们将探讨一些方法来预测常见问题以及如何解决这些问题。由于不能pickle类实例代码,因此可以添加、更改和除去方法,而不会影响现有的经过pickle的实例。出于同样的原因,可以不必担心类的属性。您必须确保包含类定义的代码模块在unpickle环境中可用。同时还必须为这些可能导致unpickle问题的更改做好规划,这些更改包括:更改类名、添加或除去实例的属性以及改变类定义模块的名称或位置。
类名的更改
要更改类名,而不破坏先前经过pickle的实例,请遵循以下步骤。首先,确保原来的类的定义没有被更改,以便在unpickle现有实例时可以找到它。不要更改原来的名称,而是在与原来类定义所在的同一个模块中,创建该类定义的一个副本,同时给它一个新的类名。然后使用实际的新类名来替代NewClassName,将以下方法添加到原来类的定义中:
清单16.更改类名:添加到原来类定义的方法
[python]viewplaincopyprint?
- def__setstate__(self,state):
- self.__dict__.update(state)
- self.__class__=NewClassName
def__setstate__(self,state):self.__dict__.update(state)self.__class__=NewClassName
当unpickle现有实例时,Python将查找原来类的定义,并调用实例的_setstate_()方法,同时将给新的类定义重新分配该实例的_class_属性。一旦确定所有现有的实例都已经unpickle、更新和重新pickle后,可以从源代码模块中除去旧的类定义。
属性的添加和删除
这些特殊的状态方法_getstate_()和_setstate_()再一次使我们能控制每个实例的状态,并使我们有机会处理实例属性中的更改。让我们看一个简单的类的定义,我们将向其添加和除去一些属性。这是是最初的定义:
清单17.最初的类定义
[python]viewplaincopyprint?
- classPerson(object):
- def__init__(self,firstname,lastname):
- self.firstname=firstname
- self.lastname=lastname
classPerson(object):def__init__(self,firstname,lastname):self.firstname=firstnameself.lastname=lastname
假定已经创建并pickle了Person的实例,现在我们决定真的只想存储一个名称属性,而不是分别存储姓和名。这里有一种方式可以更改类的定义,它将先前经过pickle的实例迁移到新的定义:
清单18.新的类定义
[python]viewplaincopyprint?
- classPerson(object):
- def__init__(self,fullname):
- self.fullname=fullname
- def__setstate__(self,state):
- if'fullname'notinstate:
- first=''
- last=''
- if'firstname'instate:
- first=state['firstname']
- delstate['firstname']
- if'lastname'instate:
- last=state['lastname']
- delstate['lastname']
- self.fullname="".join([first,last]).strip()
- self.__dict__.update(state)
classPerson(object):def__init__(self,fullname):self.fullname=fullnamedef__setstate__(self,state):if'fullname'notinstate:first=''last=''if'firstname'instate:first=state['firstname']delstate['firstname']if'lastname'instate:last=state['lastname']delstate['lastname']self.fullname="".join([first,last]).strip()self.__dict__.update(state)
在这个示例,我们添加了一个新的属性fullname,并除去了两个现有的属性firstname和lastname。当对先前进行过pickle的实例执行unpickle时,其先前进行过pickle的状态会作为字典传递给_setstate_(),它将包括firstname和lastname属性的值。接下来,将这两个值组合起来,并将它们分配给新属性fullname。在这个过程中,我们删除了状态字典中旧的属性。更新和重新pickle先前进行过pickle的所有实例之后,现在可以从类定义中除去_setstate_()方法。
模块的修改
在概念上,模块的名称或位置的改变类似于类名称的改变,但处理方式却完全不同。那是因为模块的信息存储在pickle中,而不是通过标准的pickle接口就可以修改的属性。事实上,改变模块信息的唯一办法是对实际的pickle文件本身执行查找和替换操作。至于如何确切地去做,这取决于具体的操作系统和可使用的工具。很显然,在这种情况下,您会想备份您的文件,以免发生错误。但这种改动应该非常简单,并且对二进制pickle格式进行更改与对文本pickle格式进行更改应该一样有效。
本文内容总结:
原文链接:https://www.cnblogs.com/cobbliu/archive/2012/09/04/2670178.html