Python3标准库:pickle对象串行化
1. pickle对象串行化
pickle模块实现了一个算法可以将任意的python对象转换为一系列字节。这个过程也被称为串行化对象。可以传输或存储表示对象的字节流,然后再重新构造来创建有相同性质的新对象。
1.1 编码和解码字符串中的数据
第一个例子使用dumps()将一个数据结构编码为一个字符串,然后把这个字符串打印到控制台。它使用了一个完全由内置类型构成的数据结构。任何类的实例都可以pickled,如后面的例子所示。
import pickle import pprint data = [{'a': 'a', 'b': 2, 'c': 3.0}] print('data:', end=' ') pprint.pprint(data) data_string = pickle.dumps(data) print('pickle: {!r}'.format(data_string))
默认的,pickle将以一种二进制格式写入,在python 3程序之间共享时这种格式兼容性最好。
数据串行化后,可以写到一个文件、套接字、管道或者其他位置。之后可以读取这个文件,将数据解除pickled,以便用同样的值构造一个新对象。
import pickle import pprint data1 = [{'a': 'a', 'b': 2, 'c': 3.0}] print('before: ', end=' ') pprint.pprint(data1) data1_string = pickle.dumps(data1) data2 = pickle.loads(data1_string) print('after : ', end=' ') pprint.pprint(data2) print('same? :', (data1 is data2)) print('equal?:', (data1 == data2))
新构造的对象等于原来的对象,但并不是同一个对象。
1.2 处理流
除了dumps()和loads(),pickle还提供了一些便利函数来处理类似文件的流。可以向一个流写多个对象,然后从流读取这些对象,而无须事先知道要写多少个对象或者这些对象多大。
import io import pickle class simpleobject: def __init__(self, name): self.name = name self.name_backwards = name[::-1] return data = [] data.append(simpleobject('pickle')) data.append(simpleobject('preserve')) data.append(simpleobject('last')) # simulate a file. out_s = io.bytesio() # write to the stream for o in data: print('writing : {} ({})'.format(o.name, o.name_backwards)) pickle.dump(o, out_s) out_s.flush() # set up a read-able stream in_s = io.bytesio(out_s.getvalue()) # read the data while true: try: o = pickle.load(in_s) except eoferror: break else: print('read : {} ({})'.format( o.name, o.name_backwards))
这个例子使用两个bytesio缓冲区来模拟流。第一个缓冲区接收pickled的对象,它的值被填入第二个缓冲区,load()读取这个缓冲区。简单的数据库格式也可以使用pickle来存储对象。shelve模块就是这样一个实现。
除了存储数据,pickle对于进程间通信也很方便。例如,os.fork()和os.pipe()可以用来建立工作进程,从一个管道读取作业指令,并把结果写至另一个管道。管理工作线程池以及发送作业和接收响应的核心代码可以重用,因为作业和响应对象不必基于一个特定的类。使用管道或套接字时,在转储各个对象之后不要忘记刷新输出,以便将数据通过连接推送到另一端。参见multiprocessing模块来了解一个可重用的工作线程池管理器。
1.3 重构对象的问题
处理定制类时,pickled的类必须出现在读取pickle的进程所在的命名空间里。只会pickled这个实例的数据,而不是类定义。类名用于查找构造函数,以便在解除pickled时参见新对象。下面这个例子将一个类的实例写至一个文件。
import pickleclass simpleobject: def __init__(self, name): self.name = name l = list(name) l.reverse() self.name_backwards = ''.join(l) if __name__ == '__main__': data = [] data.append(simpleobject('pickle')) data.append(simpleobject('preserve')) data.append(simpleobject('last')) with open('test.py', 'wb') as out_s: for o in data: print('writing: {} ({})'.format( o.name, o.name_backwards)) pickle.dump(o, out_s)
运行这个脚本时,会根据作为命令行参数给定的名字来创建一个文件。
通过简单的尝试加载而得到的pickled对象将会失败。
import pickle with open('test.py', 'rb') as in_s: while true: try: o = pickle.load(in_s) except eoferror: break else: print('read: {} ({})'.format( o.name, o.name_backwards))
这个版本失败的原因在于并没有simpleobject类。
修正后的版本从原脚本导入了simpleobject,这一次运行会成功。在导入列表的最后增加了import语句后,现在脚本就能找到这个类并构造对象了。
from demo import simpleobject
现在允许修改后的脚本会生成期望的结果。
1.4 unpicklable的对象
并不是所有对象都是可pickled的。套接字、文件句柄、数据库连接以及其他运行时状态依赖于操作系统或其他进程的对象,其可能无法用一种有意义的方式保存。如果对象包含不可pickled的属性,则可以定义__getstate__()和__setstate__()来返回所pickled实例的状态的一个子集。
__getstate__()方法必须返回一个对象,其中包含所pickled对象的内部状态。表示状态的一种便利方式是使用字典,不过值可以是任意的可pickled对象。保存状态,然后再从pickle加载对象时将所保存的状态传入__setstate__()。
import pickle class state: def __init__(self, name): self.name = name def __repr__(self): return 'state({!r})'.format(self.__dict__) class myclass: def __init__(self, name): print('myclass.__init__({})'.format(name)) self._set_name(name) def _set_name(self, name): self.name = name self.computed = name[::-1] def __repr__(self): return 'myclass({!r}) (computed={!r})'.format( self.name, self.computed) def __getstate__(self): state = state(self.name) print('__getstate__ -> {!r}'.format(state)) return state def __setstate__(self, state): print('__setstate__({!r})'.format(state)) self._set_name(state.name) inst = myclass('name here') print('before:', inst) dumped = pickle.dumps(inst) reloaded = pickle.loads(dumped) print('after:', reloaded)
这个例子使用了一个单独的state对象来保存myclass的内部状态。从pickle加载myclass的一个实例时,会向__setstate__()传入一个state实例,用来初始化这个对象。
1.5 循环引用
pickle协议会自动处理对象之间的循环引用,所以复杂数据结构不需要任何特殊的处理。
import pickle class node: """a simple digraph """ def __init__(self, name): self.name = name self.connections = [] def add_edge(self, node): "create an edge between this node and the other." self.connections.append(node) def __iter__(self): return iter(self.connections) def preorder_traversal(root, seen=none, parent=none): """generator function to yield the edges in a graph. """ if seen is none: seen = set() yield (parent, root) if root in seen: return seen.add(root) for node in root: recurse = preorder_traversal(node, seen, root) for parent, subnode in recurse: yield (parent, subnode) def show_edges(root): "print all the edges in the graph." for parent, child in preorder_traversal(root): if not parent: continue print('{:>5} -> {:>2} ({})'.format( parent.name, child.name, id(child))) # set up the nodes. root = node('root') a = node('a') b = node('b') c = node('c') # add edges between them. root.add_edge(a) root.add_edge(b) a.add_edge(b) b.add_edge(a) b.add_edge(c) a.add_edge(a) print('original graph:') show_edges(root) # pickle and unpickle the graph to create # a new set of nodes. dumped = pickle.dumps(root) reloaded = pickle.loads(dumped) print('\nreloaded graph:') show_edges(reloaded)
重新加载的节点并不是同一个对象,但保持了节点之间的关系,而且如果对象有多个引用,那么只会重新加载这个对象的一个副本。要验证这两点,可以在通过pickle传递节点之前和之后检查节点的id()值。