在开展跨机器交互时,常常要在机器之间传递“对象”,而不仅仅是数据,这样我们就需要将Python对象进行序列化和反系列化,前面的json其实提供了一种序列化和反序列化的机制,但仅仅支持Python的简单对象,对于复杂的对象需要自己实现序列化和反序列化编码。
Python提供了一种内部格式的序列化和反序列化机制。
模块 pickle 实现了对一个 Python 对象结构的二进制序列化和反序列化。 "pickling" 是将 Python 对象及其所拥有的层次结构转化为一个字节流的过程,而 "unpickling" 是相反的操作,会将(来自一个 binary file 或者 bytes-like object 的)字节流转化回一个对象层次结构。 pickling(和 unpickling)也被称为“序列化”, “编组” 或者 “平面化”。而为了避免混乱,此处采用术语 “封存 (pickling)” 和 “解封 (unpickling)”。
json
模块的比较在 pickle 协议和 JSON (JavaScript Object Notation) 之间有着本质上的差异:
pickle 所使用的数据格式仅可用于 Python。默认情况下,pickle 格式使用相对紧凑的二进制来存储。如果需要让文件更小,可以高效地 压缩 由 pickle 封存的数据。pickletools 模块包含了相应的工具用于分析 pickle 生成的数据流。
当前共有 6 种不同的协议可用于封存操作。 使用的协议版本越高,读取所生成 pickle 对象所需的 Python 版本就要越新。
pickle.dumps(obj, protocol=None, *, fix_imports=True, buffer_callback=None)
将obj对象序列化为bytes对象。
参数说明:
import pickle
class Base:
def __init__(self, id, desc):
self.id = id
self.desc = desc
def foo(self):
print(f'Base {self.id=}, {self.desc=}')
IDSTART = 1100001
class Teacher(Base):
def __init__(self, name, course):
self.name = name
self.course = course
class Student(Base):
def __init__(self, name, age):
self.name = name
self.age = age
self.score = {'Chinese':90, 'Math':95, 'English':90}
self.tearchers = [Teacher('Chen', 'Chinese'), Teacher('Wang', 'Math'), Teacher('Su', 'English')]
global IDSTART
IDSTART += 1
super().__init__(IDSTART, 'Student ' + name)
def foo(self):
print(f'Student {self.name=}, {self.age=}')
s1 = Student('John', 15)
ps1 = pickle.dumps(s1)
#print(ps1)
s2 = pickle.loads(ps1)
s2.foo() #Student self.name='John', self.age=15
pickle.dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None)
将序列化写入文件中。
class pickle.Pickler(file, protocol=None, *, fix_imports=True, buffer_callback=None)
它接受一个二进制文件用于写入 pickle 数据流。
主要方法:
dump(obj)
将 obj 封存后的内容写入已打开的文件对象,该文件对象已经在构造函数中指定。
persistent_id(obj)
默认无动作,子类继承重载时使用。
如果 persistent_id() 返回 None,obj 会被照常 pickle。如果返回其他值,Pickler 会将这个函数的返回值作为 obj 的持久化 ID(Pickler 本应得到序列化数据流并将其写入文件,若此函数有返回值,则得到此函数的返回值并写入文件)。这个持久化 ID 的解释应当定义在 Unpickler.persistent_load() 中(该方法定义还原对象的过程,并返回得到的对象)。注意,persistent_id() 的返回值本身不能拥有持久化 ID。
dispatch_table
Pickler 对象的 dispatch 表是 copyreg.pickle() 中用到的 reduction 函数 的注册。dispatch 表本身是一个 class 到其 reduction 函数的映射键值对。一个 reduction 函数只接受一个参数,就是其关联的 class,函数行为应当遵守 __reduce__() 接口规范。
Pickler 对象默认并没有 dispatch_table 属性,该对象默认使用 copyreg 模块中定义的全局 dispatch 表。如果要为特定 Pickler 对象自定义序列化过程,可以将 dispatch_table 属性设置为类字典对象(dict-like object)。另外,如果 Pickler 的子类设置了 dispatch_table 属性,则该子类的实例会使用这个表作为默认的 dispatch 表。
reducer_override(obj)
可以在 Pickler 的子类中定义的特殊 reducer。此方法的优先级高于 dispatch_table 中的任何 reducer。它应该与 __reduce__() 方法遵循相同的接口,它也可以返回 NotImplemented,这将使用 dispatch_table 里注册的 reducer 来封存 obj。
class pickle.PickleBuffer(buffer)
缓冲区的包装器 (wrapper),缓冲区中包含着可封存的数据。buffer 必须是一个 buffer-providing 对象,比如 bytes-like object 或多维数组。
PickleBuffer 本身就可以生成缓冲区对象,因此可以将其传递给需要缓冲区生成器的其他 API,比如 memoryview。
PickleBuffer 对象只能用 pickle 版本 5 及以上协议进行序列化。它们符合 带外序列化 的条件。
主要方法:
raw()
返回该缓冲区底层内存区域的 memoryview。 返回的对象是一维的、C 连续布局的 memoryview,格式为 B (无符号字节)。 如果缓冲区既不是 C 连续布局也不是 Fortran 连续布局的,则抛出 BufferError 异常。
release()
释放由 PickleBuffer 占用的底层缓冲区。
pickle.loads(data, /, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)
将data反序列化为一个python对象
参数说明:
pickle.load(file, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)
从文件对象中读取数据进行反序列化。
class pickle.Unpickler(file, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)
它接受一个二进制文件用于读取 pickle 数据流。
主要方法:
load()
从构造函数中指定的文件对象里读取封存好的对象,重建其中特定对象的层次结构并返回。封存对象以外的其他字节将被忽略。
persistent_load(pid)
默认抛出 UnpicklingError 异常。
如果定义了此方法,persistent_load() 应当返回持久化 ID pid 所指定的对象。 如果遇到无效的持久化 ID,则应当引发 UnpicklingError。
find_class(module, name)
如有必要,导入 module 模块并返回其中名叫 name 的对象,其中 module 和 name 参数都是 str 对象。注意,不要被这个函数的名字迷惑, find_class() 同样可以用来导入函数。
子类可以重载此方法,来控制加载对象的类型和加载对象的方式,从而尽可能降低安全风险。
通常,使一个实例可被封存不需要附加任何代码。Pickle 默认会通过 Python 的内省机制获得实例的类及属性。而当实例解封时,它的 __init__()
方法通常 不会 被调用。其默认动作是:先创建一个未初始化的实例,然后还原其属性。
import pickle
class Base:
def __init__(self, id, desc):
self.id = id
self.desc = desc
def foo(self):
print(f'Base {self.id=}, {self.desc=}')
IDSTART = 1100001
class Teacher(Base):
def __init__(self, name, course):
self.name = name
self.course = course
class Student(Base):
def __init__(self, name, age):
self.name = name
self.age = age
self.score = {'Chinese':90, 'Math':95, 'English':90}
self.tearchers = [Teacher('Chen', 'Chinese'), Teacher('Wang', 'Math'), Teacher('Su', 'English')]
global IDSTART
IDSTART += 1
super().__init__(IDSTART, 'Student ' + name)
print(f'Student init {name=}, {age=}')
def __new__(cls, *args, **kwargs):
print(f'Student new')
return super().__new__(cls)
def foo(self):
print(f'Student {self.name=}, {self.age=}')
s1 = Student('John', 15)
ps1 = pickle.dumps(s1)
#print(ps1)
s2 = pickle.loads(ps1)
s2.foo()
‘’'
Student new
Student init name='John', age=15
Student new
Student self.name='John', self.age=15
‘''
上面的例子也可以看出,只输出一次__init__()方法,在反序列化时,对象并不会再次调用__init__()方法。而__new__()方法会被再次被调用。
对于使用第 2 版或更高版协议的 pickle,实现了 __getnewargs_ex__() 方法的类可以控制在解封时传给 __new__() 方法的参数。本方法必须返回一对 (args, kwargs) 用于构建对象,其中 args 是表示位置参数的 tuple,而 kwargs 是表示命名参数的 dict。它们会在解封时传递给 __new__() 方法。
如果类的 __new__() 方法只接受关键字参数,则应当实现这个方法。否则,为了兼容性,更推荐实现 __getnewargs__() 方法。
import pickle
class Base:
def __init__(self, id, desc):
self.id = id
self.desc = desc
def foo(self):
print(f'Base {self.id=}, {self.desc=}')
IDSTART = 1100001
class Teacher(Base):
def __init__(self, name, course):
self.name = name
self.course = course
class Student(Base):
def __init__(self, name, age):
self.name = name
self.age = age
self.score = {'Chinese':90, 'Math':95, 'English':90}
self.tearchers = [Teacher('Chen', 'Chinese'), Teacher('Wang', 'Math'), Teacher('Su', 'English')]
global IDSTART
IDSTART += 1
super().__init__(IDSTART, 'Student ' + name)
print(f'Student init {name=}, {age=}')
def __new__(cls, *args):
print(f'Student new')
return super().__new__(cls)
def __getnewargs__(self):
print(f'__getnewargs__')
return (0,0)
def foo(self):
print(f'Student {self.name=}, {self.age=}')
s1 = Student('John', 15)
ps1 = pickle.dumps(s1)
#print(ps1)
s2 = pickle.loads(ps1)
s2.foo()
’’’
Student new
Student init name='John', age=15
__getnewargs__
Student new
Student self.name='John', self.age=15
‘’‘
类还可以通过重载方法 __getstate__() 来进一步影响它们的实例要如何被封存。 该方法将被调用并且其返回的对象会被当作实例的内容来封存,而不是使用默认状态。 这有几种情况:
当解封时,如果类定义了 __setstate__(),就会在已解封状态下调用它。此时不要求实例的 state 对象必须是 dict。没有定义此方法的话,先前封存的 state 对象必须是 dict,且该 dict 内容会在解封时赋给新实例的 __dict__。
如果 __getstate__() 返回 False,那么在解封时就不会调用 __setstate__() 方法。
下面的示例展示了如何修改类在封存时的行为。其中 TextReader
类打开了一个文本文件,每次调用其 readline()
方法则返回行号和该行的字符。 在封存这个 TextReader
的实例时,除了 文件对象,其他属性都会被保存。 当解封实例时,需要重新打开文件,然后从上次的位置开始继续读取。实现这些功能需要实现 __setstate__()
和 __getstate__()
方法。
class TextReader:
"""Print and number lines in a text file."""
def __init__(self, filename):
self.filename = filename
self.file = open(filename)
self.lineno = 0
def readline(self):
self.lineno += 1
line = self.file.readline()
if not line:
return None
if line.endswith('\n'):
line = line[:-1]
return "%i: %s" % (self.lineno, line)
def __getstate__(self):
# Copy the object's state from self.__dict__ which contains
# all our instance attributes. Always use the dict.copy()
# method to avoid modifying the original state.
state = self.__dict__.copy()
# Remove the unpicklable entries.
del state['file']
return state
def __setstate__(self, state):
# Restore instance attributes (i.e., filename and lineno).
self.__dict__.update(state)
# Restore the previously opened file's state. To do so, we need to
# reopen it and read from it until the line count is restored.
file = open(self.filename)
for _ in range(self.lineno):
file.readline()
# Finally, save the file.
self.file = file
>>>reader = TextReader("hello.txt")
>>>reader.readline()
'1: Hello world!'
>>>reader.readline()
'2: I am line number two.'
>>>new_reader = pickle.loads(pickle.dumps(reader))
>>>new_reader.readline()
'3: Goodbye!'
__reduce__() 方法不带任何参数,并且应返回字符串或一个元组(返回的对象通常称为“reduce 值”)。
如果返回字符串,该字符串会被当做一个全局变量的名称。它应该是对象相对于其模块的本地名称,pickle 模块会搜索模块命名空间来确定对象所属的模块。这种行为常在单例模式使用。
如果返回的是元组,则应当包含 2 到 6 个元素,可选元素可以省略或设置为 None。每个元素代表的意义如下:
作为替代选项,也可以实现 __reduce_ex__() 方法。 此方法的唯一不同之处在于它应接受一个整型参数用于指定协议版本。 如果定义了这个函数,则会覆盖 __reduce__() 的行为。 此外,__reduce__() 方法会自动成为扩展版方法的同义词。 这个函数主要用于为以前的 Python 版本提供向后兼容的 reduce 值。
为了获取对象持久化的利益, pickle 模块支持引用已封存数据流之外的对象。 这样的对象是通过一个持久化 ID 来引用的,它应当是一个由字母数字类字符组成的字符串 (对于第 0 版协议) 5 或是一个任意对象 (用于任意新版协议)。
pickle 模块不提供对持久化 ID 的解析工作,它将解析工作分配给用户定义的方法,分别是 pickler 中的 persistent_id() 方法和 unpickler 中的 persistent_load() 方法。
要通过持久化 ID 将外部对象封存,必须在 pickler 中实现 persistent_id() 方法,该方法接受需要被封存的对象作为参数,返回一个 None 或返回该对象的持久化 ID。如果返回 None,该对象会被按照默认方式封存为数据流。如果返回字符串形式的持久化 ID,则会封存这个字符串并加上一个标记,这样 unpickler 才能将其识别为持久化 ID。
要解封外部对象,Unpickler 必须实现 persistent_load() 方法,接受一个持久化 ID 对象作为参数并返回一个引用的对象。
# Simple example presenting how persistent ID can be used to pickle
# external objects by reference.
import pickle
import sqlite3
from collections import namedtuple
# Simple class representing a record in our database.
MemoRecord = namedtuple("MemoRecord", "key, task")
class DBPickler(pickle.Pickler):
def persistent_id(self, obj):
# Instead of pickling MemoRecord as a regular class instance, we emit a
# persistent ID.
if isinstance(obj, MemoRecord):
# Here, our persistent ID is simply a tuple, containing a tag and a
# key, which refers to a specific record in the database.
return ("MemoRecord", obj.key)
else:
# If obj does not have a persistent ID, return None. This means obj
# needs to be pickled as usual.
return None
class DBUnpickler(pickle.Unpickler):
def __init__(self, file, connection):
super().__init__(file)
self.connection = connection
def persistent_load(self, pid):
# This method is invoked whenever a persistent ID is encountered.
# Here, pid is the tuple returned by DBPickler.
cursor = self.connection.cursor()
type_tag, key_id = pid
if type_tag == "MemoRecord":
# Fetch the referenced record from the database and return it.
cursor.execute("SELECT * FROM memos WHERE key=?", (str(key_id),))
key, task = cursor.fetchone()
return MemoRecord(key, task)
else:
# Always raises an error if you cannot return the correct object.
# Otherwise, the unpickler will think None is the object referenced
# by the persistent ID.
raise pickle.UnpicklingError("unsupported persistent object")
def main():
import io
import pprint
# Initialize and populate our database.
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
cursor.execute("CREATE TABLE memos(key INTEGER PRIMARY KEY, task TEXT)")
tasks = (
'give food to fish',
'prepare group meeting',
'fight with a zebra',
)
for task in tasks:
cursor.execute("INSERT INTO memos VALUES(NULL, ?)", (task,))
# Fetch the records to be pickled.
cursor.execute("SELECT * FROM memos")
memos = [MemoRecord(key, task) for key, task in cursor]
# Save the records using our custom DBPickler.
file = io.BytesIO()
DBPickler(file).dump(memos)
print("Pickled records:")
pprint.pprint(memos)
# Update a record, just for good measure.
cursor.execute("UPDATE memos SET task='learn italian' WHERE key=1")
# Load the records from the pickle data stream.
file.seek(0)
memos = DBUnpickler(file, conn).load()
print("Unpickled records:")
pprint.pprint(memos)
if __name__ == '__main__':
main()
如果想对某些类进行自定义封存,而又不想在类中增加用于封存的代码,就可以创建带有特殊 dispatch 表的 pickler。
在 copyreg 模块的 copyreg.dispatch_table 中定义了全局 dispatch 表。因此,可以使用 copyreg.dispatch_table 修改后的副本作为自有 dispatch 表。
下面的例子创建了一个带有自有 dispatch 表的 pickle.Pickler 实例,它可以对 SomeClass 类进行特殊处理:
f = io.BytesIO()
p = pickle.Pickler(f)
p.dispatch_table = copyreg.dispatch_table.copy()
p.dispatch_table[SomeClass] = reduce_SomeClass
它相当于:
class MyPickler(pickle.Pickler):
dispatch_table = copyreg.dispatch_table.copy()
dispatch_table[SomeClass] = reduce_SomeClass
f = io.BytesIO()
p = MyPickler(f)
完成同样的操作,但所有 MyPickler
的实例都会共享一个私有分发表。 另一方面,代码
copyreg.pickle(SomeClass, reduce_SomeClass)
f = io.BytesIO()
p = pickle.Pickler(f)
会修改由 copyreg 模块的所有用户共享的全局分发表。
有时,dispatch_table 可能不够灵活。 特别是当我们想要基于对象类型以外的其他规则来对封存进行定制,或是当我们想要对函数和类的封存进行定制的时候。
对于那些情况,可能要基于 Pickler 类进行子类化并实现 reducer_override() 方法。 此方法可返回任意的归约元组 (参见 __reduce__())。 它也可以选择返回 NotImplemented 来回退到传统行为。
如果同时定义了 dispatch_table 和 reducer_override(),则 reducer_override() 方法具有优先权。
出于性能理由,可能不会为以下对象调用 reducer_override(): None, True, False, 以及 int, float, bytes, str, dict, set, frozenset, list 和 tuple 的具体实例。
以下是一个简单的例子,其中我们允许封存并重新构建一个给定的类:
import io
import pickle
class MyClass:
my_attribute = 1
class MyPickler(pickle.Pickler):
def reducer_override(self, obj):
"""Custom reducer for MyClass."""
if getattr(obj, "__name__", None) == "MyClass":
return type, (obj.__name__, obj.__bases__,
{'my_attribute': obj.my_attribute})
else:
# For any other object, fallback to usual reduction
return NotImplemented
f = io.BytesIO()
p = MyPickler(f)
p.dump(MyClass)
del MyClass
unpickled_class = pickle.loads(f.getvalue())
assert isinstance(unpickled_class, type)
assert unpickled_class.__name__ == "MyClass"
assert unpickled_class.my_attribute == 1
此模块包含一此能以二进制格式来读写 Python 值的函数。 这种格式是 Python 专属的,但是独立于特定的机器架构(即你可以在一台 PC 上写入某个 Python 值,将文件传到一台 Sun 上并在那里读取它)。 这种格式的细节有意不带文档说明;它可能在不同 Python 版本中发生改变(但这种情况极少发生)。
这不是一个通用的“持久化”模块。 对于通用的持久化以及通过 RPC 调用传递 Python 对象,请参阅 pickle 和 shelve 等模块。 marshal 模块主要是为了支持读写 .pyc 文件形式“伪编译”代码的 Python 模块。 因此,Python 维护者保留在必要时以不向下兼容的方式修改 marshal 格式的权利。 如果你要序列化和反序列化 Python 对象,请改用 pickle 模块 -- 其执行效率相当,版本独立性有保证,并且 pickle 还支持比 marshal 更多样的对象类型。
不是所有 Python 对象类型都受支持;一般来说,此模块只能写入和读取不依赖于特定 Python 调用的对象。 下列类型是受支持的:布尔值、整数、浮点数、复数、字符串、字节串、字节数组、元组、列表、集合、冻结集合、字典和代码对象,需要了解的一点是元组、列表、集合、冻结集合和字典只在其所包含的值也是这些值时才受支持。 单例对象 None, Ellipsis and StopIteration 也可以被 marshal 和 unmarshal。 对于 version 低于 3 的格式,递归列表、集合和字典无法被写入。
此模块的名称来源于 Modula-3 (及其他语言) 的设计者所使用的术语,他们使用术语 "marshal" 来表示以自包含的形式传输数据。 严格地说,将数据从内部形式转换为外部形式 (例如用于 RPC 缓冲区) 称为 "marshal" 而其逆过程则称为 "unmarshal"。
marshal.dump(value, file[, version])
向打开的文件写入值。 值必须为受支持的类型。 文件必须为可写的 binary file。
如果值具有(或所包含的对象具有)不受支持的类型,则会引发 ValueError --- 但是将向文件写入垃圾数据。 对象也将不能正确地通过 load() 重新读取。
version 参数指明 dump 应当使用的数据格式
marshal.load(file)
从打开的文件读取一个值并返回。 如果读不到有效的值(例如由于数据为不同 Python 版本的不兼容 marshal 格式),则会引发 EOFError, ValueError 或 TypeError。 文件必须为可读的 binary file。
marshal.dumps(value[, version])
返回将通过 dump(value, file) 被写入一个文件的字节串对象。 值必须属于受支持的类型。 如果值属于(或包含的对象属于)不受支持的类型则会引发 ValueError。
version 参数指明 dumps 应当使用的数据类型
marshal.loads(bytes)
将 bytes-like object 转换为一个值。 如果找不到有效的值,则会引发 EOFError, ValueError 或 TypeError。 输入的额外字节串会被忽略。
marshal.version
指明模块所使用的格式。 第 0 版为历史格式,第 1 版为共享固化的字符串,第 2 版对浮点数使用二进制格式。 第 3 版添加了对于对象实例化和递归的支持。 目前使用的为第 4 版。