您当前位置: 首页 潜入 Python 3

难度等级: ♦♦♦♦♢

序列化 Python 对象

从我们住进这套公寓开始,每周六我都会在 6:15 醒来,给自己倒一碗麦片,加
四分之一杯 2% 牛奶,坐在 **这** 个 **这** 个沙发的角落,打开 BBC 美国频道,观看神秘博士。
— 谢尔顿,生活大爆炸

 

深入探索

从表面上看,序列化 的概念很简单。您在内存中有一个想要保存、重用或发送给其他人的数据结构。您将如何做到这一点?嗯,这取决于您想要如何保存它、如何重用它以及要发送给谁。许多游戏允许您在退出游戏时保存进度,并在重新启动游戏时从上次离开的地方继续。 (实际上,许多非游戏应用程序也这样做。)在这种情况下,捕获“您的进度”的数据结构需要在您退出时存储在磁盘上,然后在您重新启动时从磁盘加载。这些数据仅供创建它的同一个程序使用,永远不会通过网络发送,也不会被创建它的程序以外的任何其他程序读取。因此,互操作性问题仅限于确保程序的更高版本能够读取由早期版本写入的数据。

对于这种情况,pickle 模块是理想的选择。它是 Python 标准库的一部分,因此始终可用。它速度很快;大部分代码是用 C 编写的,就像 Python 解释器本身一样。它可以存储任意复杂的 Python 数据结构。

pickle 模块可以存储什么?

如果您觉得这些还不够,pickle 模块还可以扩展。如果您对可扩展性感兴趣,请查看本章末尾“进一步阅读”部分中的链接。

关于本章示例的快速说明

本章以两个 Python Shell 为背景讲述了一个故事。本章中的所有示例都属于同一个故事线。在演示 picklejson 模块时,您将被要求在两个 Python Shell 之间来回切换。

为了帮助您理清思路,请打开 Python Shell 并定义以下变量

>>> shell = 1

请保持此窗口打开。现在打开另一个 Python Shell 并定义以下变量

>>> shell = 2

在本章中,我将使用 shell 变量来指示每个示例中使用的是哪个 Python Shell。

将数据保存到 Pickle 文件

pickle 模块与数据结构一起使用。让我们构建一个。

>>> shell 
1
>>> entry = {} 
>>> entry['title'] = 'Dive into history, 2009 edition'
>>> entry['article_link'] = 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'
>>> entry['comments_link'] = None
>>> entry['internal_id'] = b'\xDE\xD5\xB4\xF8'
>>> entry['tags'] = ('diveintopython', 'docbook', 'html')
>>> entry['published'] = True
>>> import time
>>> entry['published_date'] = time.strptime('Fri Mar 27 22:20:42 2009') 
>>> entry['published_date']
time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1)
  1. 请在 Python Shell #1 中进行操作。
  2. 这里的想法是构建一个 Python 字典,它可以代表一些有用的东西,例如 Atom Feed 中的条目。但我也想确保它包含几种不同类型的数据,以展示 pickle 模块的功能。不要对这些值进行过多的解读。
  3. time 模块包含一个数据结构(struct_time)来表示一个时间点(精确到毫秒)以及用于操作时间结构的函数。strptime() 函数接受一个格式化的字符串并将其转换为 struct_time。此字符串采用默认格式,但您可以使用格式代码对其进行控制。有关更多详细信息,请参阅 time 模块。

这是一个看起来很漂亮的 Python 字典。让我们将其保存到一个文件中。

>>> shell 
1
>>> import pickle
>>> with open('entry.pickle', 'wb') as f: 
...  pickle.dump(entry, f) 
... 
  1. 这仍然在 Python Shell #1 中。
  2. 使用 open() 函数打开一个文件。将文件模式设置为 'wb'以二进制模式 打开文件以进行写入。将其包装在 with 语句 中,以确保在您完成操作后自动关闭文件。
  3. pickle 模块中的 dump() 函数接受一个可序列化的 Python 数据结构,将其序列化为二进制的 Python 专有格式(使用最新版本的 pickle 协议),并将其保存到一个打开的文件中。

上一句话非常重要。

从 Pickle 文件加载数据

现在切换到您的第二个 Python Shell —  不是您创建 entry 字典的那个 Shell。

>>> shell 
2
>>> entry 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'entry' is not defined
>>> import pickle
>>> with open('entry.pickle', 'rb') as f: 
...  entry = pickle.load(f) 
... 
>>> entry 
{'comments_link': None,
 'internal_id': b'\xDE\xD5\xB4\xF8',
 'title': 'Dive into history, 2009 edition',
 'tags': ('diveintopython', 'docbook', 'html'),
 'article_link':
 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition',
 'published_date': time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1),
 'published': True}
  1. 这是 Python Shell #2。
  2. 这里没有定义名为 entry 的变量。您在 Python Shell #1 中定义了一个名为 entry 的变量,但这是一个完全不同的环境,拥有自己的状态。
  3. 打开您在 Python Shell #1 中创建的 entry.pickle 文件。pickle 模块使用二进制数据格式,因此您应始终以二进制模式打开 pickle 文件。
  4. pickle.load() 函数接受一个 流对象,从流中读取序列化数据,创建一个新的 Python 对象,在新 Python 对象中重新创建序列化数据,然后返回新的 Python 对象。
  5. 现在,entry 变量是一个包含熟悉键和值的字典。

pickle.dump() / pickle.load() 循环会生成一个与原始数据结构相同的新数据结构。

>>> shell 
1
>>> with open('entry.pickle', 'rb') as f: 
...  entry2 = pickle.load(f) 
... 
>>> entry2 == entry 
True
>>> entry2 is entry 
False
>>> entry2['tags'] 
('diveintopython', 'docbook', 'html')
>>> entry2['internal_id']
b'\xDE\xD5\xB4\xF8'
  1. 切换回 Python Shell #1。
  2. 打开 entry.pickle 文件。
  3. 将序列化数据加载到一个名为 entry2 的新变量中。
  4. Python 确认这两个字典 entryentry2 相等。在此 Shell 中,您从头开始构建了 entry,从一个空字典开始,手动将值分配给特定的键。您对该字典进行了序列化,并将其存储在 entry.pickle 文件中。现在,您已从该文件中读取序列化数据,并创建了原始数据结构的完美副本。
  5. 相等并不等同于同一性。我说您已创建了原始数据结构的完美副本,这是真的。但它仍然是一个副本。
  6. 出于本章后面会变得清晰的原因,我想指出 'tags' 键的值是一个元组,'internal_id' 键的值是一个 bytes 对象。

无需文件即可进行 Pickle 操作

上一节中的示例演示了如何将 Python 对象直接序列化到磁盘上的文件。但是,如果您不想或不需要文件怎么办?您也可以将 Python 对象序列化为内存中的 bytes 对象。

>>> shell
1
>>> b = pickle.dumps(entry) 
>>> type(b) 
<class 'bytes'>
>>> entry3 = pickle.loads(b) 
>>> entry3 == entry 
True
  1. pickle.dumps() 函数(注意函数名称末尾的 's')执行与 pickle.dump() 函数相同的序列化操作。它不接受流对象并将序列化数据写入磁盘上的文件,而是简单地返回序列化数据。
  2. 由于 pickle 协议使用二进制数据格式,因此 pickle.dumps() 函数会返回一个 bytes 对象。
  3. pickle.loads() 函数(同样,注意函数名称末尾的 's')执行与 pickle.load() 函数相同的反序列化操作。它不接受流对象并从文件中读取序列化数据,而是接受包含序列化数据的 bytes 对象,例如 pickle.dumps() 函数返回的对象。
  4. 最终结果相同:原始字典的完美副本。

字节和字符串再次露出丑陋的面目

pickle 协议已经存在多年,并且随着 Python 本身的成熟而不断发展。现在有四种不同的 pickle 协议版本。

看,字节和字符串之间的区别 又露出了丑陋的面目。(如果您感到惊讶,说明您没有注意。)在实践中,这意味着,虽然 Python 3 可以读取使用协议版本 2 腌制的 pickle 文件,但 Python 2 无法读取使用协议版本 3 腌制的 pickle 文件。

调试 Pickle 文件

pickle 协议是什么样子的?让我们暂时离开 Python Shell,看一下我们创建的 entry.pickle 文件。对肉眼而言,它大多是乱码。

you@localhost:~/diveintopython3/examples$ ls -l entry.pickle
-rw-r--r-- 1 you  you  358 Aug  3 13:34 entry.pickle
you@localhost:~/diveintopython3/examples$ cat entry.pickle
comments_linkqNXtagsqXdiveintopythonqXdocbookqXhtmlq?qX publishedq?
XlinkXJhttp://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition
q   Xpublished_dateq
ctime
struct_time
?qRqXtitleqXDive into history, 2009 editionqu.

这并没有什么帮助。您可以看到字符串,但其他数据类型最终会显示为不可打印(或至少不可读)的字符。字段没有被制表符或空格明显地分隔。这不是您想自己调试的格式。

>>> shell
1
>>> import pickletools
>>> with open('entry.pickle', 'rb') as f:
...     pickletools.dis(f)
    0: \x80 PROTO      3
    2: }    EMPTY_DICT
    3: q    BINPUT     0
    5: (    MARK
    6: X        BINUNICODE 'published_date'
   25: q        BINPUT     1
   27: c        GLOBAL     'time struct_time'
   45: q        BINPUT     2
   47: (        MARK
   48: M            BININT2    2009
   51: K            BININT1    3
   53: K            BININT1    27
   55: K            BININT1    22
   57: K            BININT1    20
   59: K            BININT1    42
   61: K            BININT1    4
   63: K            BININT1    86
   65: J            BININT     -1
   70: t            TUPLE      (MARK at 47)
   71: q        BINPUT     3
   73: }        EMPTY_DICT
   74: q        BINPUT     4
   76: \x86     TUPLE2
   77: q        BINPUT     5
   79: R        REDUCE
   80: q        BINPUT     6
   82: X        BINUNICODE 'comments_link'
  100: q        BINPUT     7
  102: N        NONE
  103: X        BINUNICODE 'internal_id'
  119: q        BINPUT     8
  121: C        SHORT_BINBYTES 'ÞÕ´ø'
  127: q        BINPUT     9
  129: X        BINUNICODE 'tags'
  138: q        BINPUT     10
  140: X        BINUNICODE 'diveintopython'
  159: q        BINPUT     11
  161: X        BINUNICODE 'docbook'
  173: q        BINPUT     12
  175: X        BINUNICODE 'html'
  184: q        BINPUT     13
  186: \x87     TUPLE3
  187: q        BINPUT     14
  189: X        BINUNICODE 'title'
  199: q        BINPUT     15
  201: X        BINUNICODE 'Dive into history, 2009 edition'
  237: q        BINPUT     16
  239: X        BINUNICODE 'article_link'
  256: q        BINPUT     17
  258: X        BINUNICODE 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'
  337: q        BINPUT     18
  339: X        BINUNICODE 'published'
  353: q        BINPUT     19
  355: \x88     NEWTRUE
  356: u        SETITEMS   (MARK at 5)
  357: .    STOP
highest protocol among opcodes = 3

该反汇编中最有趣的信息是在最后一行,因为它包含了用于保存该文件的 pickle 协议版本。pickle 协议中没有显式的版本标记。要确定用于存储 pickle 文件的协议版本,您需要查看腌制数据中的标记(“操作码”,opcode),并使用对每个版本的 pickle 协议中引入哪些操作码的硬编码知识。pickletools.dis() 函数的作用正是如此,它会将结果打印在反汇编输出的最后一行。以下函数仅返回版本号,而不打印任何内容

[下载 pickleversion.py]

import pickletools

def protocol_version(file_object):
    maxproto = -1
    for opcode, arg, pos in pickletools.genops(file_object):
        maxproto = max(maxproto, opcode.proto)
    return maxproto

以下是在实际情况中的应用

>>> import pickleversion
>>> with open('entry.pickle', 'rb') as f:
...     v = pickleversion.protocol_version(f)
>>> v
3

序列化 Python 对象以供其他语言读取

pickle 模块使用的数据格式是 Python 专有的。它没有尝试与其他编程语言兼容。如果跨语言兼容性是您的其中一个要求,那么您需要查看其他序列化格式。其中一种格式是 JSON。“JSON”代表“JavaScript 对象表示法”,但不要被这个名字误导 — JSON 明确设计为可在多种编程语言之间使用。

Python 3 在标准库中包含一个 json 模块。与 pickle 模块类似,json 模块也包含用于序列化数据结构、将序列化数据存储到磁盘、从磁盘加载序列化数据以及将数据反序列化回新的 Python 对象的函数。但它们之间也存在一些重要差异。首先,JSON 数据格式是基于文本的,而不是二进制的。RFC 4627 定义了 JSON 格式以及不同类型的数据如何以文本形式编码。例如,布尔值存储为五个字符的字符串 'false' 或四个字符的字符串 'true'。所有 JSON 值都区分大小写。

其次,与任何基于文本的格式一样,都会出现空格问题。 JSON 允许在值之间使用任意数量的空格(空格、制表符、回车符和换行符)。这些空格是“无关紧要的”,这意味着 JSON 编码器可以添加任意数量的空格,而 JSON 解码器必须忽略值之间的空格。这使你能够“漂亮打印”你的 JSON 数据,将值在不同缩进级别的嵌套中很好地排列,以便你可以在标准浏览器或文本编辑器中阅读。Python 的 json 模块提供了在编码期间进行漂亮打印的选项。

第三,存在字符编码的持久问题。 JSON 将值编码为纯文本,但众所周知,“纯文本”并不存在。 JSON 必须存储在 Unicode 编码(UTF-32、UTF-16 或默认的 UTF-8)中,RFC 4627 的第 3 节定义了如何识别正在使用的编码。

将数据保存到 JSON 文件

JSON 看起来非常像你在 JavaScript 中手动定义的数据结构。这并非巧合;你实际上可以使用 JavaScript 的 eval() 函数来“解码” JSON 序列化数据。(对不受信任的输入应用通常的 警告,但重点是 JSON 有效的 JavaScript。)因此,JSON 可能已经对你很熟悉。

>>> shell
1
>>> basic_entry = {} 
>>> basic_entry['id'] = 256
>>> basic_entry['title'] = 'Dive into history, 2009 edition'
>>> basic_entry['tags'] = ('diveintopython', 'docbook', 'html')
>>> basic_entry['published'] = True
>>> basic_entry['comments_link'] = None
>>> import json
>>> with open('basic.json', mode='w', encoding='utf-8') as f: 
...  json.dump(basic_entry, f) 
  1. 我们将创建一个新的数据结构,而不是重复使用现有的 entry 数据结构。本章稍后,我们将了解在尝试将更复杂的数据结构编码为 JSON 时会发生什么。
  2. JSON 是一个基于文本的格式,这意味着你需要以文本模式打开此文件并指定字符编码。始终使用 UTF-8 不会出错。
  3. pickle 模块一样,json 模块定义了一个 dump() 函数,它接受 Python 数据结构和可写流对象。dump() 函数会序列化 Python 数据结构并将其写入流对象。在 with 语句中执行此操作可以确保在完成操作时正确关闭文件。

那么,最终的 JSON 序列化结果是什么样的呢?

you@localhost:~/diveintopython3/examples$ cat basic.json
{"published": true, "tags": ["diveintopython", "docbook", "html"], "comments_link": null,
"id": 256, "title": "Dive into history, 2009 edition"}

这无疑比 pickle 文件更易读。但 JSON 可以在值之间包含任意空格,json 模块提供了一种简单的方法,可以利用这一点来创建更易读的 JSON 文件。

>>> shell
1
>>> with open('basic-pretty.json', mode='w', encoding='utf-8') as f:
...  json.dump(basic_entry, f, indent=2) 
  1. 如果你将 indent 参数传递给 json.dump() 函数,它会使生成的 JSON 文件更易读,但会以更大的文件大小为代价。 indent 参数是一个整数。0 表示“将每个值放在单独的行上”。大于 0 的数字表示“将每个值放在单独的行上,并使用此数字个空格来缩进嵌套的数据结构”。

结果如下

you@localhost:~/diveintopython3/examples$ cat basic-pretty.json
{
  "published": true, 
  "tags": [
    "diveintopython", 
    "docbook", 
    "html"
  ], 
  "comments_link": null, 
  "id": 256, 
  "title": "Dive into history, 2009 edition"
}

Python 数据类型到 JSON 的映射

由于 JSON 不是 Python 特定的,因此在它对 Python 数据类型的支持方面存在一些不匹配。其中一些仅仅是命名差异,但有两个重要的 Python 数据类型完全缺失。看看你是否能找到它们

注释 JSON Python 3
object dictionary
array list
string string
integer integer
real number float
* true True
* false False
* null None
* 所有 JSON 值都区分大小写。

你注意到哪些缺失了吗?元组 & 字节! JSON 具有一个数组类型,json 模块将其映射到 Python 列表,但它没有为“冻结数组”(元组)定义单独的类型。虽然 JSON 很好的支持字符串,但它不支持 bytes 对象或字节数组。

序列化 JSON 不支持的数据类型

即使 JSON 没有对字节的内置支持,但这并不意味着你不能序列化 bytes 对象。json 模块提供了用于编码和解码未知数据类型的扩展性挂钩。(“未知”是指“在 JSON 中未定义”。显然,json 模块知道字节数组,但它受到 JSON 规范限制。)如果要编码字节或 JSON 本身不支持的其它数据类型,你需要为这些类型提供自定义编码器和解码器。

>>> shell
1
>>> entry 
{'comments_link': None,
 'internal_id': b'\xDE\xD5\xB4\xF8',
 'title': 'Dive into history, 2009 edition',
 'tags': ('diveintopython', 'docbook', 'html'),
 'article_link': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition',
 'published_date': time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1),
 'published': True}
>>> import json
>>> with open('entry.json', 'w', encoding='utf-8') as f: 
...  json.dump(entry, f) 
... 
Traceback (most recent call last):
  File "<stdin>", line 5, in <module>
  File "C:\Python31\lib\json\__init__.py", line 178, in dump
    for chunk in iterable:
  File "C:\Python31\lib\json\encoder.py", line 408, in _iterencode
    for chunk in _iterencode_dict(o, _current_indent_level):
  File "C:\Python31\lib\json\encoder.py", line 382, in _iterencode_dict
    for chunk in chunks:
  File "C:\Python31\lib\json\encoder.py", line 416, in _iterencode
    o = _default(o)
  File "C:\Python31\lib\json\encoder.py", line 170, in default
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: b'\xDE\xD5\xB4\xF8' is not JSON serializable
  1. 好吧,现在是重新审视 entry 数据结构的时候了。它包含了所有内容:一个布尔值、一个 None 值、一个字符串、一个字符串元组、一个 bytes 对象以及一个 time 结构。
  2. 我知道之前已经说过,但值得重复一遍:JSON 是一种基于文本的格式。始终以文本模式使用 UTF-8 字符编码打开 JSON 文件。
  3. 嗯,可不好。发生了什么?

发生的事情是:json.dump() 函数尝试序列化 bytes 对象 b'\xDE\xD5\xB4\xF8',但它失败了,因为 JSON 不支持 bytes 对象。但是,如果存储字节对你很重要,你可以定义自己的“迷你序列化格式”。

[下载 customserializer.py]


def to_json(python_object):                                             
    if isinstance(python_object, bytes):                                
        return {'__class__': 'bytes',
                '__value__': list(python_object)}                       
    raise TypeError(repr(python_object) + ' is not JSON serializable')  
  1. 要为 JSON 本身不支持的数据类型定义自己的“迷你序列化格式”,只需定义一个函数,它将 Python 对象作为参数。这个 Python 对象将是 json.dump() 函数本身无法序列化的实际对象,在本例中,是 bytes 对象 b'\xDE\xD5\xB4\xF8'
  2. 你的自定义序列化函数应该检查 json.dump() 函数传递给它的 Python 对象的类型。如果你的函数只序列化一种数据类型,这并不是严格必要的,但它可以清楚地表明你的函数涵盖了哪种情况,并且如果以后需要添加更多数据类型的序列化,它会更容易扩展。
  3. 在本例中,我选择将 bytes 对象转换为字典。__class__ 键将保存原始数据类型(作为字符串 'bytes'),__value__ 键将保存实际值。当然,它不能是 bytes 对象;整个要点是将其转换为可以以 JSON 格式序列化的东西!bytes 对象只是一系列整数;每个整数都在 0–255 的范围内。我们可以使用 list() 函数将 bytes 对象转换为整数列表。因此,b'\xDE\xD5\xB4\xF8' 变为 [222, 213, 180, 248]。(计算一下!它可以工作!十六进制字节 \xDE 在十进制中是 222,\xD5 是 213,依此类推)。
  4. 这一行很重要。你正在序列化的数据结构可能包含内置 JSON 序列化器或你的自定义序列化器都无法处理的类型。在这种情况下,你的自定义序列化器必须引发 TypeError,以便 json.dump() 函数知道你的自定义序列化器未识别该类型。

就是这样;你无需做任何其他操作。尤其是,这个自定义序列化函数返回一个 Python 字典,而不是字符串。你并没有自己完成整个序列化为 JSON 的操作;你只完成了转换为支持的数据类型的部分。json.dump() 函数将完成剩下的工作。

>>> shell
1
>>> import customserializer 
>>> with open('entry.json', 'w', encoding='utf-8') as f: 
...  json.dump(entry, f, default=customserializer.to_json) 
... 
Traceback (most recent call last):
  File "<stdin>", line 9, in <module>
    json.dump(entry, f, default=customserializer.to_json)
  File "C:\Python31\lib\json\__init__.py", line 178, in dump
    for chunk in iterable:
  File "C:\Python31\lib\json\encoder.py", line 408, in _iterencode
    for chunk in _iterencode_dict(o, _current_indent_level):
  File "C:\Python31\lib\json\encoder.py", line 382, in _iterencode_dict
    for chunk in chunks:
  File "C:\Python31\lib\json\encoder.py", line 416, in _iterencode
    o = _default(o)
  File "/Users/pilgrim/diveintopython3/examples/customserializer.py", line 12, in to_json
    raise TypeError(repr(python_object) + ' is not JSON serializable')                     
TypeError: time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1) is not JSON serializable
  1. customserializer 模块是你在上一示例中定义 to_json() 函数的地方。
  2. 文本模式、UTF-8 编码,等等。(你会忘记!我有时也会忘记!并且一切都将正常工作,直到它失败的那一刻,到那时它将以最壮观的方式失败)。
  3. 这是重点:要将你的自定义转换函数挂钩到 json.dump() 函数,将你的函数传递到 json.dump() 函数的 default 参数中。(万岁,Python 中的一切都是对象!)。
  4. 好吧,它并没有真正奏效。但请查看异常。json.dump() 函数不再抱怨无法序列化 bytes 对象。现在它抱怨的是一个完全不同的对象:time.struct_time 对象。

虽然得到不同的异常可能看起来没有进展,但实际上确实如此!只需要再调整一下就能克服这个障碍。


import time

def to_json(python_object):
    if isinstance(python_object, time.struct_time):          
        return {'__class__': 'time.asctime',
                '__value__': time.asctime(python_object)}    
    if isinstance(python_object, bytes):
        return {'__class__': 'bytes',
                '__value__': list(python_object)}
    raise TypeError(repr(python_object) + ' is not JSON serializable')
  1. 在我们的现有 customserializer.to_json() 函数中,我们需要检查 Python 对象(json.dump() 函数难以处理的对象)是否是 time.struct_time
  2. 如果是,我们将执行与处理 bytes 对象时类似的操作:将 time.struct_time 对象转换为仅包含 JSON 可序列化值的字典。在本例中,将日期时间转换为 JSON 可序列化值的最简单方法是使用 time.asctime() 函数将其转换为字符串。time.asctime() 函数将那个看起来很糟糕的 time.struct_time 转换为字符串 'Fri Mar 27 22:20:42 2009'

有了这两个自定义转换,整个 entry 数据结构应该可以序列化为 JSON,而不会出现任何进一步的问题。

>>> shell
1
>>> with open('entry.json', 'w', encoding='utf-8') as f:
...     json.dump(entry, f, default=customserializer.to_json)
... 
you@localhost:~/diveintopython3/examples$ ls -l example.json
-rw-r--r-- 1 you  you  391 Aug  3 13:34 entry.json
you@localhost:~/diveintopython3/examples$ cat example.json
{"published_date": {"__class__": "time.asctime", "__value__": "Fri Mar 27 22:20:42 2009"},
"comments_link": null, "internal_id": {"__class__": "bytes", "__value__": [222, 213, 180, 248]},
"tags": ["diveintopython", "docbook", "html"], "title": "Dive into history, 2009 edition",
"article_link": "http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition",
"published": true}

JSON 文件加载数据

pickle 模块一样,json 模块有一个 load() 函数,它接受一个流对象,从中读取 JSON 编码的数据,并创建一个新的 Python 对象,该对象反映了 JSON 数据结构。

>>> shell
2
>>> del entry 
>>> entry
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'entry' is not defined
>>> import json
>>> with open('entry.json', 'r', encoding='utf-8') as f:
...  entry = json.load(f) 
... 
>>> entry 
{'comments_link': None,
 'internal_id': {'__class__': 'bytes', '__value__': [222, 213, 180, 248]},
 'title': 'Dive into history, 2009 edition',
 'tags': ['diveintopython', 'docbook', 'html'],
 'article_link': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition',
 'published_date': {'__class__': 'time.asctime', '__value__': 'Fri Mar 27 22:20:42 2009'},
 'published': True}
  1. 为了演示目的,请切换到 Python Shell #2 并删除本章前面使用 pickle 模块创建的 entry 数据结构。
  2. 在最简单的情况下,json.load() 函数的工作方式与 pickle.load() 函数相同。你传入一个流对象,它会返回一个新的 Python 对象。
  3. 我有好消息和坏消息。先说好消息:json.load() 函数成功读取了你之前在 Python Shell #1 中创建的 entry.json 文件,并创建了一个包含该数据的新的 Python 对象。现在说坏消息:它没有重新创建原始的 entry 数据结构。两个值 'internal_id''published_date' 被重新创建为字典,更具体地说,是包含你在 to_json() 转换函数中创建的 JSON 兼容值的字典。

json.load() 对你可能传递给 json.dump() 的任何转换函数一无所知。你需要的是 to_json() 函数的反面,一个函数可以接受自定义转换的 JSON 对象并将其转换回原始 Python 数据类型。

# add this to customserializer.py
def from_json(json_object):                                   
    if '__class__' in json_object:                            
        if json_object['__class__'] == 'time.asctime':
            return time.strptime(json_object['__value__'])    
        if json_object['__class__'] == 'bytes':
            return bytes(json_object['__value__'])            
    return json_object
  1. 此转换函数也接受一个参数并返回一个值。但它接受的参数不是字符串,而是一个 Python 对象,即将 JSON 编码的字符串反序列化为 Python 的结果。
  2. 你只需要检查这个对象是否包含 to_json() 函数创建的 '__class__' 键。如果是,'__class__' 键的值将告诉你如何将该值解码回原始 Python 数据类型。
  3. 要解码 time.asctime() 函数返回的时间字符串,你可以使用 time.strptime() 函数。此函数接受格式化的日期时间字符串(使用可定制的格式,但它默认为与 time.asctime() 的默认格式相同的格式),并返回一个 time.struct_time
  4. 要将整数列表转换回 bytes 对象,可以使用 bytes() 函数。

就是这样;to_json() 函数只处理了两种数据类型,现在这两种数据类型也被from_json() 函数处理了。这是结果。

>>> shell
2
>>> import customserializer
>>> with open('entry.json', 'r', encoding='utf-8') as f:
...  entry = json.load(f, object_hook=customserializer.from_json) 
... 
>>> entry 
{'comments_link': None,
 'internal_id': b'\xDE\xD5\xB4\xF8',
 'title': 'Dive into history, 2009 edition',
 'tags': ['diveintopython', 'docbook', 'html'],
 'article_link': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition',
 'published_date': time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1),
 'published': True}
  1. 为了将from_json() 函数挂接到反序列化过程中,将其作为 object_hook 参数传递给 json.load() 函数。函数接收函数;太方便了!
  2. 现在 entry 数据结构包含一个'internal_id' 键,其值为 bytes 对象。它还包含一个'published_date' 键,其值为 time.struct_time 对象。

不过,还有一个最终的故障。

>>> shell
1
>>> import customserializer
>>> with open('entry.json', 'r', encoding='utf-8') as f:
...     entry2 = json.load(f, object_hook=customserializer.from_json)
... 
>>> entry2 == entry 
False
>>> entry['tags'] 
('diveintopython', 'docbook', 'html')
>>> entry2['tags'] 
['diveintopython', 'docbook', 'html']
  1. 即使将to_json() 函数挂接到序列化,并将from_json() 函数挂接到反序列化,我们仍然没有重新创建原始数据结构的完美副本。为什么?
  2. 在原始 entry 数据结构中,'tags' 键的值是一个包含三个字符串的元组。
  3. 但在经过循环处理的 entry2 数据结构中,'tags' 键的值是一个包含三个字符串的列表JSON 不区分元组和列表;它只有一个列表式数据类型,即数组,json 模块在序列化期间会将元组和列表静默地转换为 JSON 数组。对于大多数用途,您可以忽略元组和列表之间的差异,但在使用 json 模块时,这需要注意。

进一步阅读

许多关于 pickle 模块的文章引用了 cPickle。在 Python 2 中,pickle 模块有两个实现,一个是用纯 Python 编写的,另一个是用 C 编写的(但仍然可以从 Python 调用)。在 Python 3 中,这两个模块已经合并,因此您应该始终只使用 import pickle。您可能会发现这些文章有用,但您应该忽略有关 cPickle 的过时信息。

关于使用 pickle 模块进行序列化

关于 JSONjson 模块

关于 pickle 扩展性

© 2001–11 Mark Pilgrim