Python 持久性管理
什么是持久性?
对象持久性
一些经过 pickle 的 Python
pickle
模块及其同类模块 cPickle
向 Python 提供了 pickle支持。后者是用 C 编码的,它具有更好的性能,对于大多数应用程序,推荐使用该模块。我们将继续讨论pickle
,但本文的示例实际是利用了 cPickle
。由于其中大多数示例要用 Python shell来显示,所以先展示一下如何导入cPickle
,并可以作为 pickle
来引用它:>>> import cPickle as 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 是二进制格式还是文本格式。dumps()
和 loads()
函数:清单 1. dumps() 和 loads() 的演示
>>> import cPickle as pickle
>>> t1 = ('this is a string', 42, [1, 2, 3], None)
>>> t1
('this is a string', 42, [1, 2, 3], None)
>>> p1 = pickle.dumps(t1)
>>> p1
"(S'this is a string'/nI42/n(lp1/nI1/naI2/naI3/naNtp2/n."
>>> print p1
(S'this is a string'
I42
(lp1
I1
aI2
aI3
aNtp2
.
>>> t2 = pickle.loads(p1)
>>> t2
('this is a string', 42, [1, 2, 3], None)
>>> p2 = pickle.dumps(t1, True)
>>> p2
'(U/x10this is a stringK*]q/x01(K/x01K/x02K/x03eNtq/x02.'
>>> t3 = pickle.loads(p2)
>>> t3
('this is a string', 42, [1, 2, 3], None)
pickle
模块中记录了所有使用的约定。我们还应该指出,在我们的示例中使用的都是简单对象,因此使用二进制 pickle格式不会在节省空间上显示出太大的效率。然而,在实际使用复杂对象的系统中,您会看到,使用二进制格式可以在大小和速度方面带来显著的改进。dump()
和 load()
,它们使用文件和类似文件的对象。这些函数的操作非常类似于我们刚才所看到的dumps()
和 loads()
,区别在于它们还有另一种能力 — dump()
函数能一个接着一个地将几个对象转储到同一个文件。随后调用load()
来以同样的顺序检索这些对象。清单 2 显示了这种能力的实际应用:清单 2. dump() 和 load() 示例
>>> 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
模块中提供了有关目前以及所支持的格式方面的详细信息:清单 3. 检索所支持的格式
多个引用,同一对象
清单 4. 对象引用的维护
循环引用和递归引用
>清单 5. 递归引用
>>> 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. 循环引用
>>> 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] is b
1
>>> b[2] is a
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] is d
1
>>> d[2] is c
1
清单 7. 分别 pickle vs. 在一个元组中一起 pickle
>>> 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] is d
0
>>> d[2] is c
0
相等,但并不总是相同
清单 8. 作为原来对象副本的被恢复的对象
>>> j = [1, 2, 3]
>>> k = j
>>> k is j
1
>>> x = pickle.dumps(k)
>>> y = pickle.loads(x)
>>> y
[1, 2, 3]
>>> y == k
1
>>> y is k
0
>>> y is j
0
>>> k is j
1
dump()
会使 Python 无法维护对在该单元外部进行 pickle的对象的引用。相反,Python 复制了被引用对象,并将副本和被 pickle 的对象存储在一起。对于 pickle和恢复单个对象层次结构的应用程序,这是没有问题的。但要意识到还有其它情形。pickle
和cPickle
模块提供了一个 Pickler
(与此相对应是 Unpickler
),它能够跟踪已经被pickle 的对象。通过使用这个Pickler
,将会通过引用而不是通过值来 pickle 共享和循环引用:清单 9. 维护分别 pickle 的对象间的引用
>>> f = file('temp.pkl', 'w')
>>> pickler = pickle.Pickler(f)
>>> pickler.dump(a)
<cPickle.Pickler object at 0x89b0bb8>
>>> pickler.dump(b)
<cPickle.Pickler object at 0x89b0bb8>
>>> 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] is d
1
>>> d[2] is c
1
不可 pickle 的对象
清单 10. 试图 pickle 文件对象的结果
>>> f = file('temp.pkl', 'w')
>>> p = pickle.dumps(f)
Traceback (most recent call last):
File "<input>", line 1, in ?
File "/usr/lib/python2.2/copy_reg.py", line 57, in _reduce
raise TypeError, "can't pickle %s objects" % base.__name__
TypeError: can't pickle file objects
类实例
_dict_
属性)和类的名称,而不会 pickle 类的代码。当 Python unpickle类的实例时,它会试图使用在 pickle 该实例时的确切的类名称和模块名称(包括任何包的路径前缀)导入包含该类定义的模块。另外要注意,类定义必须出现在模块的最顶层,这意味着它们不能是嵌套的类(在其它类或函数中定义的类)。_init_()
方法。相反,Python 创建一个通用类实例,并应用已进行过 pickle的实例属性,同时设置该实例的_class_
属性,使其指向原来的类。copy_reg
模块的 _reconstructor()
函数来恢复新型类的实例。_getstate_()
和 _setstate_()
,在保存和恢复类实例的状态信息期间,Python会调用这些方法。在以下几节中,我们会看到一些示例利用了这些特殊的方法。persist.py
的 Python模块,它包含以下新型类的定义:清单 11. 新型类的定义
class Foo(object):
def __init__(self, value):
self.value = value
Foo
实例,并看一下它的表示:清单 12. pickle Foo 实例
>>> import cPickle as pickle
>>> from Orbtech.examples.persist import Foo
>>> foo = Foo('What is a Foo?')
>>> p = pickle.dumps(foo)
>>> print p
ccopy_reg
_reconstructor
p1
(cOrbtech.examples.persist
Foo
p2
c__builtin__
object
p3
NtRp4
(dp5
S'value'
p6
S'What is a Foo?'
sb.
>>>
Foo
和全限定的模块名称 Orbtech.examples.persist
都存储在 pickle中。如果将这个实例 pickle 成一个文件,稍后再 unpickle它或在另一台机器上 unpickle,则 Python 会试图导入Orbtech.examples.persist
模块,如果不能导入,则会抛出异常。如果重命名该类和该模块或者将该模块移到另一个目录,则也会发生类似的错误。Foo
类,然后试图装入先前进行过pickle 的 Foo
实例时会发生该错误:清单 13. 试图装入一个被重命名的 Foo 类的经过 pickle 的实例
>>> import cPickle as pickle
>>> f = file('temp.pkl', 'r')
>>> foo = pickle.load(f)
Traceback (most recent call last):
File "<input>", line 1, in ?
AttributeError: 'module' object has no attribute 'Foo'
persist.py
模块之后,也会发生类似的错误:清单 14. 试图装入一个被重命名的 persist.py 模块的经过 pickle 的实例
>>> import cPickle as pickle
>>> f = file('temp.pkl', 'r')
>>> foo = pickle.load(f)
Traceback (most recent call last):
File "<input>", line 1, in ?
ImportError: No module named persist
特殊的状态方法
_getstate_()
和_setstate_()
)来修改类实例的状态。这里有一个 Foo
类的示例,我们已经对它进行了修改以处理文件对象属性:清单 15. 处理不能 pickle 的实例属性
class Foo(object):
def __init__(self, value, filename):
self.value = value
self.logfile = file(filename, 'w')
def __getstate__(self):
"""Return state values to be pickled."""
f = self.logfile
return (self.value, f.name, f.tell())
def __setstate__(self, state):
"""Restore state from the unpickled state values."""
self.value, name, position = state
f = file(name, 'w')
f.seek(position)
self.logfile = f
Foo
的实例时,Python 将只 pickle 当它调用该实例的 _getstate_()
方法时返回给它的值。类似的,在 unpickle 时,Python 将提供经过 unpickle 的值作为参数传递给实例的_setstate_()
方法。在 _setstate_()
方法内,可以根据经过 pickle 的名称和位置信息来重建文件对象,并将该文件对象分配给这个实例的logfile
属性。模式改进
类名的更改
NewClassName
,将以下方法添加到原来类的定义中:清单 16. 更改类名:添加到原来类定义的方法
def __setstate__(self, state):
self.__dict__.update(state)
self.__class__ = NewClassName
_setstate_()
方法,同时将给新的类定义重新分配该实例的_class_
属性。一旦确定所有现有的实例都已经 unpickle、更新和重新 pickle 后,可以从源代码模块中除去旧的类定义。属性的添加和删除
_getstate_()
和 _setstate_()
再一次使我们能控制每个实例的状态,并使我们有机会处理实例属性中的更改。让我们看一个简单的类的定义,我们将向其添加和除去一些属性。这是是最初的定义:清单 17. 最初的类定义
class Person(object):
def __init__(self, firstname, lastname):
self.firstname = firstname
self.lastname = lastname
Person
的实例,现在我们决定真的只想存储一个名称属性,而不是分别存储姓和名。这里有一种方式可以更改类的定义,它将先前经过pickle 的实例迁移到新的定义:清单 18. 新的类定义
class Person(object):
def __init__(self, fullname):
self.fullname = fullname
def __setstate__(self, state):
if 'fullname' not in state:
first = ''
last = ''
if 'firstname' in state:
first = state['firstname']
del state['firstname']
if 'lastname' in state:
last = state['lastname']
del state['lastname']
self.fullname = " ".join([first, last]).strip()
self.__dict__.update(state)
fullname
,并除去了两个现有的属性 firstname
和 lastname
。当对先前进行过 pickle 的实例执行 unpickle 时,其先前进行过 pickle的状态会作为字典传递给 _setstate_()
,它将包括firstname
和 lastname
属性的值。接下来,将这两个值组合起来,并将它们分配给新属性 fullname
。在这个过程中,我们删除了状态字典中旧的属性。更新和重新 pickle先前进行过 pickle 的所有实例之后,现在可以从类定义中除去_setstate_()
方法。模块的修改
结束语
(版权归原作者所有,侵删)
点击下方“阅读原文”查看更多
评论