# Chapter 13 序列化Python對象
> " Every Saturday since we’ve lived in this apartment, I have awakened at 6:15, poured myself a bowl of cereal, added
> a quarter-cup of 2% milk, sat on **this** end of **this** couch, turned on BBC America, and watched Doctor Who. "
> — Sheldon, [The Big Bang Theory](http://en.wikiquote.org/wiki/The_Big_Bang_Theory#The_Dumpling_Paradox_.5B1.07.5D)
## 深入
序列化的概念很簡單。內存里面有一個數據結構,你希望將它保存下來,重用,或者發送給其他人。你會怎么做?嗯, 這取決于你想要怎么保存,怎么重用,發送給誰。很多游戲允許你在退出的時候保存進度,然后你再次啟動的時候回到上次退出的地方。(實際上, 很多非游戲程序也會這么干。) 在這個情況下, 一個捕獲了當前進度的數據結構需要在你退出的時候保存到磁盤上,接著在你重新啟動的時候從磁盤上加載進來。這個數據只會被創建它的程序使用,不會發送到網絡上,也不會被其它程序讀取。因此,互操作的問題被限制在保證新版本的程序能夠讀取以前版本的程序創建的數據。
在這種情況下,`pickle` 模塊是理想的。它是Python標準庫的一部分, 所以它總是可用的。它很快; 它的大部分同Python解釋器本身一樣是用C寫的。 它可以存儲任意復雜的Python數據結構。
什么東西能用`pickle`模塊存儲?
* 所有Python支持的 [原生類型](native-datatypes.html) : 布爾, 整數, 浮點數, 復數, 字符串, `bytes`(字節串)對象, 字節數組, 以及 `None`.
* 由任何原生類型組成的列表,元組,字典和集合。
* 由任何原生類型組成的列表,元組,字典和集合組成的列表,元組,字典和集合(可以一直嵌套下去,直至[Python支持的最大遞歸層數](http://docs.python.org/3.1/library/sys.html#sys.getrecursionlimit "sys.getrecursionlimit()")).
* 函數,類,和類的實例(帶警告)。
如果這還不夠用,`pickle`模塊也是可擴展的。如果你對可擴展性有興趣,請查看本章最后的[進一步閱讀](#furtherreading)小節中的鏈接。
### 本章例子的快速筆記
本章會使用兩個Python Shell來講故事。本章的例子都是一個單獨的故事的一部分。當我演示`pickle` 和 `json` 模塊時,你會被要求在兩個Python Shell中來回切換。
為了讓事情簡單一點,打開Python Shell 并定義下面的變量:
```
>>> shell = 1
```
保持該窗口打開。 現在打開另一個Python Shell 并定義下面下面的變量:
```
>>> shell = 2
```
貫穿整個章節, 在每個例子中我會使用`shell`變量來標識使用的是哪個Python Shell。
## 保存數據到 Pickle 文件
`pickle`模塊的工作對象是數據結構。讓我們來創建一個:
```
1
>>> 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.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 供稿的entry](xml.html#xml-structure)。但是為了炫耀一下`pickle`模塊我也想保證里面包含了多種不同的數據類型。不需要太關心這些值。
3. `time` 模塊包含一個表示時間點(精確到1毫秒)的數據結構(`time_struct`)以及操作時間結構的函數。`strptime()`函數接受一個格式化過的字符串并將其轉化成一個`time_struct`。這個字符串使用的是默認格式,但你可以通過格式化代碼來控制它。查看[`time`模塊](http://docs.python.org/3.1/library/time.html)來獲得更多細節。
這是一個很帥的Python 字典。讓我們把它保存到文件。
```
1
>>> import pickle
...
```
1. 仍然在Python Shell #1 中。
2. 使用`open()` 函數來打開一個文件。設置文件模式為`'wb'`來以[二進制](files.html#binary)寫模式打開文件。把它放入[`with` 語句](files.html#with)中來保證在你完成的時候文件自動被關閉。
3. `pickle`模塊中的`dump()`函數接受一個可序列化的Python 數據結構, 使用最新版本的pickle協議將其序列化為一個二進制的,Python特定的格式, 并且保存到一個打開的文件里。
最后一句話很重要。
* `pickle`模塊接受一個Python數據結構并將其保存的一個文件。
* 要做到這樣,它使用一個被稱為“pickle協議”的東西_序列化_該數據結構。
* pickle 協議是Python特定的,沒有任何跨語言兼容的保證。你很可能不能使用Perl, PHP, Java, 或者其他語言來對你剛剛創建的`entry.pickle`文件做任何有用的事情。
* 并非所有的Python數據結構都可以通過`pickle`模塊序列化。隨著新的數據類型被加入到Python語言中,pickle協議已經被修改過很多次了,但是它還是有一些限制。
* 由于這些變化,不同版本的Python的兼容性也沒有保證。新的版本的Python支持舊的序列化格式,但是舊版本的Python不支持新的格式(因為它們不支持新的數據類型)。
* 除非你指定,`pickle`模塊中的函數將使用最新版本的pickle協議。這保證了你對可以被序列化的數據類型有最大的靈活度,但這也意味著生成的文件不能被不支持新版pickle協議的舊版本的Python讀取。
* 最新版本的pickle協議是二進制格式的。請確認使用[二進制模式](files.html#binary)來打開你的pickle文件,否則當你寫入的時候數據會被損壞。
## 從Pickle文件讀取數據
現在切換到你的第二個Python Shell?—?_即_不是你創建`entry`字典的那個。
```
2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'entry' is not defined
>>> import pickle
...
{'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()`函數接受一個[流對象](files.html#file-objects), 從流中讀取序列化后的數據,創建一個新的Python對象,在新的Python對象中重建被序列化的數據,然后返回新建的Python對象。
5. 現在`entry`變量是一個鍵和值看起來都很熟悉的字典。
`pickle.dump() / pickle.load()`循環的結果是一個和原始數據結構等同的新的數據結構。
```
1
...
True
False
('diveintopython', 'docbook', 'html')
>>> entry2['internal_id']
b'\xDE\xD5\xB4\xF8'
```
1. 切換回Python Shell #1。
2. 打開`entry.pickle`文件。
3. 將序列化后的數據裝載到一個新的變量, `entry2`。
4. Python 確認兩個字典, `entry` 和 `entry2` 是相等的。在這個shell里, 你從零開始構造了`entry`, 從一個空字典開始然后手工給各個鍵賦值。你序列化了這個字典并將其保存在`entry.pickle`文件中。現在你從文件中讀取序列化后的數據并創建了原始數據結構的一個完美復制品。
5. 相等和相同是不一樣的。我說的是你創建了原始數據結構的一個_完美復制品_, 這沒錯。但它僅僅是一個復制品。
6. 我要指出`'tags'`鍵對應的值是一個元組,而`'internal_id'`鍵對應的值是一個`bytes`對象。原因在這章的后面就會清楚了。
## 不使用文件來進行序列化
前一節中的例子展示了如果將一個Python對象序列化到磁盤文件。但如果你不想或不需要文件呢?你也可以序列化到一個內存中的`bytes`對象。
```
>>> shell
1
<class 'bytes'>
True
```
1. `pickle.dumps()`函數(注意函數名最后的`'s'`)執行和`pickle.dump()`函數相同的序列化。取代接受流對象并將序列化后的數據保存到磁盤文件,這個函數簡單的返回序列化的數據。
2. 由于pickle協議使用一個二進制數據格式,所以`pickle.dumps()`函數返回`bytes`對象。
3. `pickle.loads()`函數(再一次, 注意函數名最后的`'s'`) 執行和`pickle.load()`函數一樣的反序列化。取代接受一個流對象并去文件讀取序列化后的數據,它接受包含序列化后的數據的`bytes`對象, 比如`pickle.dumps()`函數返回的對象。
4. 最終結果是一樣的: 原始字典的完美復制。
## 字節串和字符串又一次抬起了它們丑陋的頭。
pickle協議已經存在好多年了,它隨著Python本身的成熟也不斷成熟。現在存在[四個不同版本](http://docs.python.org/3.1/library/pickle.html#data-stream-format) 的pickle協議。
* Python 1.x 有兩個pickle協議,一個基于文本的格式(“版本 0”) 以及一個二進制格式(“版本 1”).
* Python 2.3 引入了一個新的pickle協議(“版本 2”) 來處理Python 類對象的新功能。它是一個二進制格式。
* Python 3.0 引入了另一個pickle 協議 (“版本 3”) ,顯式的支持`bytes` 對象和字節數組。它是一個二進制格式。
你看, [字節串和字符串的區別](strings.html#byte-arrays)又一次抬起了它們丑陋的頭。 (如果你覺得驚奇,你肯定開小差了。) 在實踐中這意味著, 盡管Python 3 可以讀取版本 2 的pickle 協議生成的數據, Python 2 不能讀取版本 3的協議生成的數據.
## 調試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
<mark>highest protocol among opcodes = 3</mark>
```
這個反匯編中最有趣的信息是最后一行, 因為它包含了文件保存時使用的pickle協議的版本號。在pickle協議里面沒有明確的版本標志。為了確定保存pickle文件時使用的協議版本,你需要查看序列化后的數據的標記(“opcodes”)并且使用硬編碼的哪個版本的協議引入了哪些標記的知識(來確定版本號)。`pickle.dis()`函數正是這么干的,并且它在反匯編的輸出的最后一行打印出結果。下面是一個不打印,僅僅返回版本號的函數:
[[下載 `pickleversion.py`](examples/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](http://json.org/)。 “JSON” 代表 “JavaScript Object Notation,” 但是不要讓名字糊弄你。?—?JSON 是被設計為跨語言使用的。
Python 3 在標準庫中包含了一個 `json`模塊。同 `pickle`模塊類似, `json`模塊包含一些函數,可以序列化數據結構,保存序列化后的數據至磁盤,從磁盤上讀取序列化后的數據,將數據反序列化成新的Pythone對象。但兩者也有一些很重要的區別。 首先, JSON數據格式是基于文本的, 不是二進制的。[RFC 4627](http://www.ietf.org/rfc/rfc4627.txt) 定義了JSON格式以及怎樣將各種類型的數據編碼成文本。比如,一個布爾值要么存儲為5個字符的字符串`'false'`,要么存儲為4個字符的字符串 `'true'`。 所有的JSON值都是大小寫敏感的。
第二,由于是文本格式, 存在空白(whitespaces)的問題。 JSON 允許在值之間有任意數目的空白(空格, 跳格, 回車,換行)。空白是“無關緊要的”,這意味著JSON編碼器可以按它們的喜好添加任意多或任意少的空白, 而JSON解碼器被要求忽略值之間的任意空白。這允許你“美觀的打印(pretty-print)” 你的 JSON 數據, 通過不同的縮進層次嵌套值,這樣你就可以在標準瀏覽器或文本編輯器中閱讀它。Python 的 `json` 模塊有在編碼時執行美觀打印(pretty-printing)的選項。
第三, 字符編碼的問題是長期存在的。JSON 用純文本編碼數據, 但是你知道, [“不存在純文本這種東西。”](strings.html) JSON必須以Unicode 編碼(UTF-32, UTF-16, 或者默認的, UTF-8)方式存儲, [RFC 4627的第3節](http://www.ietf.org/rfc/rfc4627.txt) 定義了如何區分使用的是哪種編碼。
## 將數據保存至 JSON 文件
JSON 看起來非常像你在Javascript中手工定義的數據結構。這不是意外; 實際上你可以使用JavaScript 的`eval()`函數來“解碼” JSON序列化過的數據。(通常的[對非信任輸入的警告](advanced-iterators.html#eval) 也適用, 但關鍵點是JSON _是_ 合法的JavaScript。) 因此, 你可能已經熟悉JSON了。
```
>>> shell
1
>>> 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
```
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 文件更可讀](#debugging)。然而 JSON 的值之間可以包含任意數目的空把, 并且`json`模塊提供了一個方便的途徑來利用這一點生成更可讀的JSON文件。
```
>>> shell
1
>>> with open('basic-pretty.json', mode='w', encoding='utf-8') as f:
```
1. 如果你給`json.dump()`函數傳入`indent`參數, 它以文件變大為代價使生成的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](native-datatypes.html#dictionaries) |
| | array | [list](native-datatypes.html#lists) |
| | string | [string](strings.html#divingin) |
| | integer | [integer](native-datatypes.html#numbers) |
| | real number | [float](native-datatypes.html#numbers) |
| * | `true` | [`True`](native-datatypes.html#booleans) |
| * | `false` | [`False`](native-datatypes.html#booleans) |
| * | `null` | `[None](native-datatypes.html#none)` |
| * | 所有的 JSON 值都是大小寫敏感的。 |
注意到什么被遺漏了嗎?元組和 _&_ 字節串(bytes)! JSON 有數組類型, `json` 模塊將其映射到Python的列表, 但是它沒有一個單獨的類型對應 “凍結數組(frozen arrays)” (元組)。而且盡管 JSON 非常好的支持字符串,但是它沒有對`bytes` 對象或字節數組的支持。
## 序列化JSON不支持的數據類型
即使JSON沒有內建的字節流支持, 并不意味著你不能序列化`bytes`對象。`json`模塊提供了編解碼未知數據類型的擴展接口。(“未知”的意思是?JSON沒有定義”。很顯然`json` 模塊認識字節數組, 但是它被JSON規范的限制束縛住了。) 如果你希望編碼字節串或者其它JSON沒有原生支持的數據類型,你需要給這些類型提供定制的編碼和解碼器。
```
>>> shell
1
{'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
...
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")
<mark>TypeError: b'\xDE\xD5\xB4\xF8' is not JSON serializable</mark>
```
1. 好的, 是時間再看看`entry` 數據結構了。它包含了所有的東西: 布爾值,`None`值,字符串,字符串元組, `bytes`對象, 以及`time`結構體。
2. 我知道我已經說過了,但是這值得再重復一次:JSON 是一個基于文本的格式。總是應使用UTF-8字符編碼以文本模式打開JSON文件。
3. 嗯,_這_可不好。發生什么了?
情況是這樣的: `json.dump()` 函數試圖序列化`bytes`對象 `b'\xDE\xD5\xB4\xF8'`,但是它失敗了,原因是JSON 不支持`bytes`對象。然而, 如果保存字節串對你來說很重要,你可以定義自己的“迷你序列化格式。”
1. 為了給一個JSON沒有原生支持的數據類型定義你自己的“迷你序列化格式”, 只要定義一個接受一個Python對象為參數的函數。這個對象將會是`json.dump()`函數無法自己序列化的實際對象?—?這個例子里是`bytes` 對象 `b'\xDE\xD5\xB4\xF8'`。
2. 你的自定義序列化函數應該檢查`json.dump()`函數傳給它的對象的類型。當你的函數只序列化一個類型的時候這不是必須的,但是它使你的函數的覆蓋的內容清楚明白,并且在你需要序列化更多類型的時候更容易擴展。
3. 在這個例子里面, 我將`bytes` 對象轉換成字典。`__class__` 鍵持有原始的數據類型(以字符串的形式, `'bytes'`), 而 `__value__` 鍵持有實際的數據。當然它不能是`bytes`對象; 大體的想法是將其轉換成某些可以被JSON序列化的東西! `bytes`對象就是一個范圍在0–255的整數的序列。 我們可以使用`list()` 函數將`bytes`對象轉換成整數列表。所以`b'\xDE\xD5\xB4\xF8'` 變成 `[222, 213, 180, 248]`. (算一下! 這是對的! 16進制的字節 `\xDE` 是十進制的 222, `\xD5` 是 213, 以此類推。)
4. 這一行很重要。你序列化的數據結構可能包含JSON內建的可序列化類型和你的定制序列化器支持的類型之外的東西。在這種情況下,你的定制序列化器拋出一個`TypeError`,那樣`json.dump()` 函數就可以知道你的定制序列化函數不認識該類型。
就這么多;你不需要其它的東西。特別是, 這個定制序列化函數_返回Python字典_,不是字符串。你不是自己做所有序列化到JSON的工作; 你僅僅在做轉換成被支持的類型那部分工作。`json.dump()` 函數做剩下的事情。
```
>>> shell
1
...
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
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 編碼, yadda yadda。(你很可能會忘記這一點! 我就忘記過好幾次! 事情一切正常直到它失敗的時刻, 而它的失敗很令人矚目。)
3. 這是重點: 為了將定制轉換函數鉤子嵌入`json.dump()`函數, 只要將你的函數以`default`參數傳入`json.dump()`函數。(萬歲, [Python里一切皆對象](your-first-python-program.html#everythingisanobject)!)
4. 好吧, 實際上還是不能工作。但是看一下異常。`json.dump()` 函數不再抱怨無法序列化`bytes`對象了。現在它在抱怨另一個完全不同的對象: `time.struct_time` 對象。
盡管得到另一個不同的異常看起來不是什么進步, 但它確實是個進步! 再調整一下就可以解決這個問題。
```
import time
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. 在現存的`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編碼過的數據, 并且創建該JSON數據結構的Python對象的鏡像。
```
>>> shell
2
>>> 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:
...
{'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
if json_object['__class__'] == 'time.asctime':
if json_object['__class__'] == 'bytes':
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:
...
{'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` 數據結構現在有一個值為`bytes`對象的`'internal_id'`鍵。它也包含一個`'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)
...
False
('diveintopython', 'docbook', 'html')
['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中, [這兩個模塊已經合并](porting-code-to-python-3-with-2to3.html#othermodules), 所以你總是簡單的`import pickle`就可以。你可能會發現這些文章很有用,但是你應該忽略已過時的關于的`cPickle`的信息.
使用`pickle`模塊打包:
* [`pickle` module](http://docs.python.org/3.1/library/pickle.html)
* [`pickle` and `cPickle`?—?Python object serialization](http://www.doughellmann.com/PyMOTW/pickle/)
* [Using `pickle`](http://wiki.python.org/moin/UsingPickle)
* [Python persistence management](http://www.ibm.com/developerworks/library/l-pypers.html)
使用JSON 和 `json` 模塊:
* [`json`?—?JavaScript Object Notation Serializer](http://www.doughellmann.com/PyMOTW/json/)
* [JSON encoding and ecoding with custom objects in Python](http://blog.quaternio.net/2009/07/16/json-encoding-and-decoding-with-custom-objects-in-python/)
擴展打包:
* [Pickling class instances](http://docs.python.org/3.1/library/pickle.html#pickling-class-instances)
* [Persistence of external objects](http://docs.python.org/3.1/library/pickle.html#persistence-of-external-objects)
* [Handling stateful objects](http://docs.python.org/3.1/library/pickle.html#handling-stateful-objects)
- 版權信息
- Chapter -1 《深入 Python 3》中有何新內容
- Chapter 0 安裝 Python
- Chapter 1 你的第一個 Python 程序
- Chapter 2 內置數據類型
- Chapter 3 解析
- Chapter 4 字符串
- Chapter 5 正則表達式
- Chapter 6 閉合 與 生成器
- Chapter 7 類 & 迭代器
- Chapter 8 高級迭代器
- Chapter 9 單元測試
- Chapter 10 重構
- Chapter 11 文件
- Chapter 12 XML
- Chapter 13 序列化Python對象
- Chapter 14 HTTP Web 服務
- Chapter 15 案例研究:將chardet移植到Python 3
- Chapter 16 打包 Python 類庫
- Chapter A 使用2to3將代碼移植到Python 3
- Chapter B 特殊方法名稱
- Chapter C 接下來閱讀什么?