12.4 序列化
在程序運(yùn)行的過(guò)程中,所有的變量都是在內(nèi)存中,比如,定義一個(gè)dict:
可以隨時(shí)修改變量,比如把name
改成'Bill'
,但是一旦程序結(jié)束,變量所占用的內(nèi)存就被操作系統(tǒng)全部回收。如果沒(méi)有把修改后的'Bill'
存儲(chǔ)到磁盤(pán)上,下次重新運(yùn)行程序,變量又被初始化為'Bob'
。
我們把變量從內(nèi)存中變成可存儲(chǔ)或傳輸?shù)倪^(guò)程稱(chēng)之為序列化,在Python中叫pickling,在其他語(yǔ)言中也被稱(chēng)之為serialization,marshalling,flattening等等,都是一個(gè)意思。
序列化之后,就可以把序列化后的內(nèi)容寫(xiě)入磁盤(pán),或者通過(guò)網(wǎng)絡(luò)傳輸?shù)絼e的機(jī)器上。
反過(guò)來(lái),把變量?jī)?nèi)容從序列化的對(duì)象重新讀到內(nèi)存里稱(chēng)之為反序列化,即unpickling。
Python提供了pickle
模塊來(lái)實(shí)現(xiàn)序列化。
首先,我們嘗試把一個(gè)對(duì)象序列化并寫(xiě)入文件:
pickle.dumps()
方法把任意對(duì)象序列化成一個(gè)bytes
,然后,就可以把這個(gè)bytes
寫(xiě)入文件?;蛘哂昧硪粋€(gè)方法pickle.dump()
直接把對(duì)象序列化后寫(xiě)入一個(gè)file-like Object:
看看寫(xiě)入的dump.txt
文件,一堆亂七八糟的內(nèi)容,這些都是Python保存的對(duì)象內(nèi)部信息。
當(dāng)我們要把對(duì)象從磁盤(pán)讀到內(nèi)存時(shí),可以先把內(nèi)容讀到一個(gè)bytes
,然后用pickle.loads()
方法反序列化出對(duì)象,也可以直接用pickle.load()
方法從一個(gè)file-like Object
中直接反序列化出對(duì)象。我們打開(kāi)另一個(gè)Python命令行來(lái)反序列化剛才保存的對(duì)象:
變量的內(nèi)容又回來(lái)了!
當(dāng)然,這個(gè)變量和原來(lái)的變量是完全不相干的對(duì)象,它們只是內(nèi)容相同而已。
Pickle的問(wèn)題和所有其他編程語(yǔ)言特有的序列化問(wèn)題一樣,就是它只能用于Python,并且可能不同版本的Python彼此都不兼容,因此,只能用Pickle保存那些不重要的數(shù)據(jù),不能成功地反序列化也沒(méi)關(guān)系。
JSON
如果我們要在不同的編程語(yǔ)言之間傳遞對(duì)象,就必須把對(duì)象序列化為標(biāo)準(zhǔn)格式,比如XML,但更好的方法是序列化為JSON,因?yàn)镴SON表示出來(lái)就是一個(gè)字符串,可以被所有語(yǔ)言讀取,也可以方便地存儲(chǔ)到磁盤(pán)或者通過(guò)網(wǎng)絡(luò)傳輸。JSON不僅是標(biāo)準(zhǔn)格式,并且比XML更快,而且可以直接在Web頁(yè)面中讀取,非常方便。
JSON表示的對(duì)象就是標(biāo)準(zhǔn)的JavaScript語(yǔ)言的對(duì)象,JSON和Python內(nèi)置的數(shù)據(jù)類(lèi)型對(duì)應(yīng)如下:

Python內(nèi)置的json
模塊提供了非常完善的Python對(duì)象到JSON格式的轉(zhuǎn)換。我們先看看如何把Python對(duì)象變成一個(gè)JSON:
dumps()
方法返回一個(gè)str
,內(nèi)容就是標(biāo)準(zhǔn)的JSON。類(lèi)似的,dump()
方法可以直接把JSON寫(xiě)入一個(gè)file-like Object
。
要把JSON反序列化為Python對(duì)象,用loads()
或者對(duì)應(yīng)的load()
方法,前者把JSON的字符串反序列化,后者從file-like Object
中讀取字符串并反序列化:
由于JSON標(biāo)準(zhǔn)規(guī)定JSON編碼是UTF-8,所以我們總是能正確地在Python的str
與JSON的字符串之間轉(zhuǎn)換。
JSON進(jìn)階
Python的dict
對(duì)象可以直接序列化為JSON的{}
,不過(guò),很多時(shí)候,我們更喜歡用class
表示對(duì)象,比如定義Student
類(lèi),然后序列化:
運(yùn)行代碼,毫不留情地得到一個(gè)TypeError
:
錯(cuò)誤的原因是Student
對(duì)象不是一個(gè)可序列化為JSON的對(duì)象。
如果連class
的實(shí)例對(duì)象都無(wú)法序列化為JSON,這肯定不合理!
別急,我們仔細(xì)看看dumps()
方法的參數(shù)列表,可以發(fā)現(xiàn),除了第一個(gè)必須的obj
參數(shù)外,dumps()
方法還提供了一大堆的可選參數(shù):
https://docs.python.org/3/library/json.html#json.dumps
這些可選參數(shù)就是讓我們來(lái)定制JSON序列化。前面的代碼之所以無(wú)法把Student
類(lèi)實(shí)例序列化為JSON,是因?yàn)槟J(rèn)情況下,dumps()
方法不知道如何將Student
實(shí)例變?yōu)橐粋€(gè)JSON的{}
對(duì)象。
可選參數(shù)default
就是把任意一個(gè)對(duì)象變成一個(gè)可序列為JSON的對(duì)象,我們只需要為Student
專(zhuān)門(mén)寫(xiě)一個(gè)轉(zhuǎn)換函數(shù),再把函數(shù)傳進(jìn)去即可:
這樣,Student
實(shí)例首先被student2dict()
函數(shù)轉(zhuǎn)換成dict
,然后再被順利序列化為JSON:
不過(guò),下次如果遇到一個(gè)Teacher
類(lèi)的實(shí)例,照樣無(wú)法序列化為JSON。我們可以偷個(gè)懶,把任意class
的實(shí)例變?yōu)?code>dict:
因?yàn)橥ǔ?code>class的實(shí)例都有一個(gè)__dict__
屬性,它就是一個(gè)dict
,用來(lái)存儲(chǔ)實(shí)例變量。也有少數(shù)例外,比如定義了__slots__
的class。
同樣的道理,如果我們要把JSON反序列化為一個(gè)Student
對(duì)象實(shí)例,loads()
方法首先轉(zhuǎn)換出一個(gè)dict
對(duì)象,然后,我們傳入的object_hook
函數(shù)負(fù)責(zé)把dict
轉(zhuǎn)換為Student
實(shí)例:
運(yùn)行結(jié)果如下:
打印出的是反序列化的Student
實(shí)例對(duì)象。
小結(jié)
Python語(yǔ)言特定的序列化模塊是pickle
,但如果要把序列化搞得更通用、更符合Web標(biāo)準(zhǔn),就可以使用json
模塊。
json
模塊的dumps()
和loads()
函數(shù)是定義得非常好的接口的典范。當(dāng)我們使用時(shí),只需要傳入一個(gè)必須的參數(shù)。但是,當(dāng)默認(rèn)的序列化或反序列機(jī)制不滿足我們的要求時(shí),我們又可以傳入更多的參數(shù)來(lái)定制序列化或反序列化的規(guī)則,既做到了接口簡(jiǎn)單易用,又做到了充分的擴(kuò)展性和靈活性。