python序列化數據本地存放

深刻#

序列化的概念很簡單。內存裏面有一個數據結構,你但願將它保存下來,重 用,或者發送給其餘人。你會怎麼作?嗯, 這取決於你想要怎麼保存,怎麼重用,發送給誰。不少遊戲容許你在退出的時候保存進度,而後你再次啓動的時候回到上次退出的地方。(實際上, 不少非遊戲程序也會這麼幹。) 在這個狀況下, 一個捕獲了當前進度的數據結構須要在你退出的時候保存到磁盤上,接着在你從新啓動的時候從磁盤上加載進來。這個數據只會被建立它的程序使用,不會發送到網 絡上,也不會被其它程序讀取。所以,互操做的問題被限制在保證新版本的程序可以讀取之前版本的程序建立的數據。php

在這種狀況下,pickle 模塊是理想的。它是Python標準庫的一部分, 因此它老是可用的。它很快; 它的大部分同Python解釋器自己同樣是用C寫的。 它能夠存儲任意複雜的Python數據結構。html

什麼東西能用pickle模塊存儲?python

  • 全部Python支持的 原生類型 : 布爾, 整數, 浮點數, 複數, 字符串, bytes(字節串)對象, 字節數組, 以及 None.shell

  • 由任何原生類型組成的列表,元組,字典和集合。編程

  • 由任何原生類型組成的列表,元組,字典和集合組成的列表,元組,字典和集合(能夠一直嵌套下去,直至Python支持的最大遞歸層數).json

  • 函數,類,和類的實例(帶警告)。數組

若是這還不夠用,pickle模塊也是可擴展的。若是你對可擴展性有興趣,請查看本章最後的進一步閱讀小節中的連接。瀏覽器

本章例子的快速筆記#

本章會使用兩個Python Shell來說故事。本章的例子都是一個單獨的故事的一部分。當我演示pickle 和 json 模塊時,你會被要求在兩個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)
在Python Shell #1 裏面。
想法是創建一個Python字典來表示一些有用的東西,好比一個Atom 供稿的entry。可是爲了炫耀一下pickle模塊我也想保證裏面包含了多種不一樣的數據類型。不須要太關心這些值。
time 模塊包含一個表示時間點(精確到1毫秒)的數據結構(time_struct)以及操做時間結構的函數。strptime()函數接受一個格式化過的字符串並將其轉化成一個time_struct。這個字符串使用的是默認格式,但你能夠經過格式化代碼來控制它。查看time模塊來得到更多細節。

這是一個很帥的Python 字典。讓咱們把它保存到文件。

跳過該代碼清單

[隱藏] [在新窗口中打開]>>> shell①1>>> import pickle>>> with open('entry.pickle', 'wb') as f:②...     pickle.dump(entry, f)③...
仍然在Python Shell #1 中。
使用open() 函數來打開一個文件。設置文件模式爲'wb'來以二進制寫模式打開文件。把它放入with 語句中來保證在你完成的時候文件自動被關閉。
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協議是二進制格式的。請確認使用二進制模式來打開你的pickle文件,不然當你寫入的時候數據會被損壞。

從Pickle文件讀取數據#

如今切換到你的第二個Python Shell — 不是你建立entry字典的那個。

跳過該代碼清單

[隱藏] [在新窗口中打開]>>> 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}
這是Python Shell #2.
這裏沒有entry 變量被定義過。你在Python Shell #1 中定義了entry變量, 可是那是另外一個擁有本身狀態的徹底不一樣的環境。
打開你在Python Shell #1中建立的entry.pickle文件。pickle模塊使用二進制數據格式,因此你老是應該使用二進制模式打開pickle文件。
pickle.load()函數接受一個流對象, 從流中讀取序列化後的數據,建立一個新的Python對象,在新的Python對象中重建被序列化的數據,而後返回新建的Python對象。
如今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'
切換回Python Shell #1。
打開entry.pickle文件。
將序列化後的數據裝載到一個新的變量, entry2。
Python 確認兩個字典, entry 和 entry2 是相等的。在這個shell裏, 你從零開始構造了entry, 從一個空字典開始而後手工給各個鍵賦值。你序列化了這個字典並將其保存在entry.pickle文件中。如今你從文件中讀取序列化後的數據並建立了原始數據結構的一個完美複製品。
相等和相同是不同的。我說的是你建立了原始數據結構的一個完美複製品, 這沒錯。但它僅僅是一個複製品。
我要指出'tags'鍵對應的值是一個元組,而'internal_id'鍵對應的值是一個bytes對象。緣由在這章的後面就會清楚了。

不使用文件來進行序列化#

前一節中的例子展現了若是將一個Python對象序列化到磁盤文件。但若是你不想或不須要文件呢?你也能夠序列化到一個內存中的bytes對象。

跳過該代碼清單

[隱藏] [在新窗口中打開]>>> shell1>>> b = pickle.dumps(entry)①>>> type(b)②<class 'bytes'>>>> entry3 = pickle.loads(b)③>>> entry3 == entry④True
pickle.dumps()函數(注意函數名最後的's')執行和pickle.dump()函數相同的序列化。取代接受流對象並將序列化後的數據保存到磁盤文件,這個函數簡單的返回序列化的數據。
因爲pickle協議使用一個二進制數據格式,因此pickle.dumps()函數返回bytes對象。
pickle.loads()函數(再一次, 注意函數名最後的's') 執行和pickle.load()函數同樣的反序列化。取代接受一個流對象並去文件讀取序列化後的數據,它接受包含序列化後的數據的bytes對象, 好比pickle.dumps()函數返回的對象。
最終結果是同樣的: 原始字典的完美複製。

字節串和字符串又一次擡起了它們醜陋的頭。#

pickle協議已經存在好多年了,它隨着Python自己的成熟也不斷成熟。如今存在四個不一樣版本 的pickle協議。

  • Python 1.x 有兩個pickle協議,一個基於文本的格式(「版本 0」) 以及一個二進制格式(「版本 1」).

  • Python 2.3 引入了一個新的pickle協議(「版本 2」) 來處理Python 類對象的新功能。它是一個二進制格式。

  • Python 3.0 引入了另外一個pickle 協議 (「版本 3」) ,顯式的支持bytes 對象和字節數組。它是一個二進制格式。

你看, 字節串和字符串的區別又一次擡起了它們醜陋的頭。 (若是你以爲驚奇,你確定開小差了。) 在實踐中這意味着, 儘管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.pickleyou@localhost:~/diveintopython3/examples$ cat entry.picklecomments_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.

這不是頗有用。你能夠看見字符串,可是其餘數據類型顯示爲不可打印的(或者至少是不可讀的)字符。域之間沒有明顯的分隔符(好比跳格符或空格)。你確定不但願來調試這樣一個格式。

跳過該代碼清單

[隱藏] [在新窗口中打開]>>> shell1>>> 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文件時使用的協議版本,你 須要查看序列化後的數據的標記(「opcodes」)而且使用硬編碼的哪一個版本的協議引入了哪些標記的知識(來肯定版本號)。pickle.dis()函數正是這麼幹的,而且它在反彙編的輸出的最後一行打印出結果。下面是一個不打印,僅僅返回版本號的函數:

跳過該代碼清單

[隱藏] [在新窗口中打開] [下載 pickleversion.py]import pickletoolsdef 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)>>> v3

序列化Python對象以供其它語言讀取#

pickle模塊使用的數據格式是Python特定的。它沒有作任何兼容其它編程語言的努力。若是跨語言兼容是你的需求之一,你得去尋找其它的序列化格式。一個這樣的格式是json。 「json」 表明 「JavaScript Object Notation,」 可是不要讓名字糊弄你。 — json 是被設計爲跨語言使用的。

Python 3 在標準庫中包含了一個 json模塊。同 pickle模塊相似, json模塊包含一些函數,能夠序列化數據結構,保存序列化後的數據至磁盤,從磁盤上讀取序列化後的數據,將數據反序列化成新的Pythone對象。但二者也有一些很重要的區別。 首先, json數據格式是基於文本的, 不是二進制的。RFC 4627 定義了json格式以及怎樣將各類類型的數據編碼成文本。好比,一個布爾值要麼存儲爲5個字符的字符串'false',要麼存儲爲4個字符的字符串 'true'。 全部的json值都是大小寫敏感的。

第二,因爲是文本格式, 存在空白(whitespaces)的問題。 json 容許在值之間有任意數目的空白(空格, 跳格, 回車,換行)。空白是「可有可無的」,這意味着json編碼器能夠按它們的喜愛添加任意多或任意少的空白, 而json解碼器被要求忽略值之間的任意空白。這容許你「美觀的打印(pretty-print)」 你的 json 數據, 經過不一樣的縮進層次嵌套值,這樣你就能夠在標準瀏覽器或文本編輯器中閱讀它。Python 的 json 模塊有在編碼時執行美觀打印(pretty-printing)的選項。

第三, 字符編碼的問題是長期存在的。json 用純文本編碼數據, 可是你知道, 「不存在純文本這種東西。」 json必須以Unicode 編碼(UTF-32, UTF-16, 或者默認的, utf-8)方式存儲,RFC 4627的第3節 定義瞭如何區分使用的是哪一種編碼。

將數據保存至 json 文件#

json 看起來很是像你在Javascript中手工定義的數據結構。這不是意外; 實際上你可使用JavaScript 的eval()函數來「解碼」 json序列化過的數據。(一般的對非信任輸入的警告也適用, 但關鍵點是json  合法的JavaScript。) 所以, 你可能已經熟悉json了。

跳過該代碼清單

[隱藏] [在新窗口中打開]>>> shell1>>> 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)③
咱們將建立一個新的數據結構,而不是重用現存的entry數據結構。在這章的後面, 咱們將會看見當咱們試圖用json編碼更復雜的數據結構的時候會發生什麼。
json 是一個基於文本的格式, 這意味你能夠以文本模式打開文件,並給定一個字符編碼。用utf-8老是沒錯的。
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文件。

跳過該代碼清單

[隱藏] [在新窗口中打開]>>> shell1>>> with open('basic-pretty.json', mode='w', encoding='utf-8') as f:...     json.dump(basic_entry, f, indent=2)①
若是你給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
  array list
  string string
  integer integer
  real number float
* true True
* false False
* null None
* 全部的 json 值都是大小寫敏感的。

注意到什麼被遺漏了嗎?元組和 & 字節串(bytes)! json 有數組類型, json 模塊將其映射到Python的列表, 可是它沒有一個單獨的類型對應 「凍結數組(frozen arrays)」 (元組)。並且儘管 json 很是好的支持字符串,可是它沒有對bytes 對象或字節數組的支持。

序列化json不支持的數據類型#

即便json沒有內建的字節流支持, 並不意味着你不能序列化bytes對象。json模塊提供了編解碼未知數據類型的擴展接口。(「未知」的意思是≴json沒有定義」。很顯然json 模塊認識字節數組, 可是它被json規範的限制束縛住了。) 若是你但願編碼字節串或者其它json沒有原生支持的數據類型,你須要給這些類型提供定製的編碼和解碼器。

跳過該代碼清單

[隱藏] [在新窗口中打開]>>> shell1>>> 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
好的, 是時間再看看entry 數據結構了。它包含了全部的東西: 布爾值,None值,字符串,字符串元組, bytes對象, 以及time結構體。
我知道我已經說過了,可是這值得再重複一次:json 是一個基於文本的格式。老是應使用utf-8字符編碼以文本模式打開json文件。
嗯,可很差。發生什麼了?

狀況是這樣的: json.dump() 函數試圖序列化bytes對象 b'\xDE\xD5\xB4\xF8',可是它失敗了,緣由是json 不支持bytes對象。然而, 若是保存字節串對你來講很重要,你能夠定義本身的「迷你序列化格式。」

跳過該代碼清單

[隱藏] [在新窗口中打開] [download 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')  ④
爲了給一個json沒有原生支持的數據類型定義你本身的「迷你序列化格式」, 只要定義一個接受一個Python對象爲參數的函數。這個對象將會是json.dump()函數沒法本身序列化的實際對象 — 這個例子裏是bytes 對象 b'\xDE\xD5\xB4\xF8'
你的自定義序列化函數應該檢查json.dump()函數傳給它的對象的類型。當你的函數只序列化一個類型的時候這不是必須的,可是它使你的函數的覆蓋的內容清楚明白,而且在你須要序列化更多類型的時候更容易擴展。
在這個例子裏面, 我將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, 以此類推。)
這一行很重要。你序列化的數據結構可能包含json內建的可序列化類型和你的定製序列化器支持的類型以外的東西。在這種狀況下,你的定製序列化器拋出一個TypeError,那樣json.dump() 函數就能夠知道你的定製序列化函數不認識該類型。

就這麼多;你不須要其它的東西。特別是, 這個定製序列化函數返回Python字典,不是字符串。你不是本身作全部序列化到json的工做; 你僅僅在作轉換成被支持的類型那部分工做。json.dump() 函數作剩下的事情。

跳過該代碼清單

[隱藏] [在新窗口中打開]>>> shell1>>> 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
customserializer 模塊是你在前一個例子中定義to_json()函數的地方。
文本模式, utf-8 編碼, yadda yadda。(你極可能會忘記這一點! 我就忘記過好幾回! 事情一切正常直到它失敗的時刻, 而它的失敗很使人矚目。)
這是重點: 爲了將定製轉換函數鉤子嵌入json.dump()函數, 只要將你的函數以default參數傳入json.dump()函數。(萬歲, Python裏一切皆對象!)
好吧, 實際上仍是不能工做。可是看一下異常。json.dump() 函數再也不抱怨沒法序列化bytes對象了。如今它在抱怨另外一個徹底不一樣的對象: time.struct_time 對象。

儘管獲得另外一個不一樣的異常看起來不是什麼進步, 但它確實是個進步! 再調整一下就能夠解決這個問題。

跳過該代碼清單

[隱藏] [在新窗口中打開]import timedef 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')
在現存的customserializer.to_json()函數裏面, 咱們加入了Python 對象 (json.dump() 處理不了的那些) 是否是 time.struct_time的判斷。
若是是的,咱們作一些同處理bytes對象時相似的事情來轉換: 將time.struct_time 結構轉化成一個只包含json可序列化值的字典。在這個例子裏, 最簡單的將日期時間轉換成json可序列化值的方法是使用time.asctime()函數將其轉換成字符串。time.asctime() 函數將難看的time.struct_time 轉換成字符串 'Fri Mar 27 22:20:42 2009'

有了兩個定製的轉換, 整個entry 數據結構序列化到json應該沒有進一步的問題了。

跳過該代碼清單

[隱藏] [在新窗口中打開]>>> shell1>>> 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.jsonyou@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對象的鏡像。

跳過該代碼清單

[隱藏] [在新窗口中打開]>>> shell2>>> del entry①>>> entryTraceback (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}
爲了演示目的,切換到Python Shell #2 而且刪除在這一章前面使用pickle模塊建立的entry數據結構。
最簡單的狀況下,json.load()函數同pickle.load()函數的結果如出一轍。你傳入一個流對象,它返回一個新的Python對象。
有好消息也有壞消息。好消息先來: 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.pydef 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
這函數也一樣接受一個參數返回一個值。可是參數不是字符串,而是一個Python對象 — 反序列化一個json編碼的字符串爲Python的結果。
你只須要檢查這個對象是否包含to_json()函數建立的'__class__'鍵。若是是的,'__class__'鍵對應的值將告訴你如何將值解碼成原來的Python數據類型。
爲了解碼由time.asctime()函數返回的字符串,你要使用time.strptime()函數。這個函數接受一個格式化過的時間字符串(格式能夠自定義,但默認值同time.asctime()函數的默認值相同) 而且返回time.struct_time.
爲了將整數列表轉換回bytes 對象, 你可使用 bytes() 函數。

就是這樣; to_json()函數處理了兩種數據類型,如今這兩個數據類型也在from_json()函數裏面處理了。下面是結果:

跳過該代碼清單

[隱藏] [在新窗口中打開]>>> shell2>>> 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}
爲了將from_json()函數嵌入到反序列化過程當中,把它做爲object_hook 參數傳入到json.load()函數中。接受函數做爲參數的函數; 真方便!
entry 數據結構如今有一個值爲bytes對象的'internal_id'鍵。它也包含一個'published_date'鍵,其值爲time.struct_time對象。

然而,還有最後一個缺陷。

跳過該代碼清單

[隱藏] [在新窗口中打開]>>> shell1>>> 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']
即便在序列化過程當中加入了to_json()鉤子函數, 也在反序列化過程當中加入from_json()鉤子函數, 咱們仍然沒有從新建立原始數據結構的完美複製品。爲何沒有?
在原始的entry 數據結構中, 'tags'鍵的值爲一個三個字符串組成的元組。
可是重現建立的entry2 數據結構中, 'tags' 鍵的值是一個三個字符串組成的列表。json 並不區分元組和列表;它只有一個相似列表的數據類型,數組,而且json模塊在序列化過程當中會安靜的將元組和列表兩個都轉換成json 數組。大多數狀況下,你能夠忽略元組和列表的區別,可是在使用json 模塊時應記得有這麼一回使。

進一步閱讀#

不少關於pickle模塊的文章提到了cPickle。在Python 2中, pickle 模塊有兩個實現, 一個由純Python寫的而另外一個用C寫的(但仍然能夠在Python中調用)。在Python 3中, 這兩個模塊已經合併, 因此你老是簡單的import pickle就能夠。你可能會發現這些文章頗有用,可是你應該忽略已過期的關於的cPickle的信息.

使用pickle模塊打包:

使用json 和 json 模塊:

擴展打包:

相關文章
相關標籤/搜索