天天看點

[Python]持久存儲

持久性的基本思想很簡單。假定有一個 python 程式,它可能是一個管理日常待辦事項的程式,您希望在多次執行這個程式之間可以儲存應用程式對象(待辦事項)。換句話說,您希望将對象存儲在磁盤上,便于以後檢索。這就是持久性。要達到這個目的,有幾種方法,每一種方法都有其優缺點。

例如,可以将對象資料存儲在某種格式的文本檔案中,譬如 csv 檔案。或者可以用關系資料庫,譬如 gadfly、mysql、postgresql 或者 db2。這些檔案格式和資料庫都非常優秀,對于所有這些存儲機制,python 都有健壯的接口。

這些存儲機制都有一個共同點:存儲的資料是獨立于對這些資料進行操作的對象和程式。這樣做的好處是,資料可以作為共享的資源,供其它應用程式使用。缺點是,用這種方式,可以允許其它程式通路對象的資料,這違背了面向對象的封裝性原則 — 即對象的資料隻能通過這個對象自身的公共(public)接口來通路。

另外,對于某些應用程式,關系資料庫方法可能不是很理想。尤其是,關系資料庫不了解對象。相反,關系資料庫會強行使用自己的類型系統和關系資料模型(表),每張表包含一組元組(行),每行包含具有固定數目的靜态類型字段(列)。如果應用程式的對象模型不能夠友善地轉換到關系模型,那麼在将對象映射到元組以及将元組映射回對象方面,會碰到一定難度。這種困難常被稱為阻礙性不比對(impedence-mismatch)問題。

如果希望透明地存儲 python 對象,而不丢失其身份和類型等資訊,則需要某種形式的對象序列化:它是一個将任意複雜的對象轉成對象的文本或二進制表示的過程。同樣,必須能夠将對象經過序列化後的形式恢複到原有的對象。在 python 中,這種序列化過程稱為 pickle,可以将對象 pickle 成字元串、磁盤上的檔案或者任何類似于檔案的對象,也可以将這些字元串、檔案或任何類似于檔案的對象

unpickle 成原來的對象。我們将在本文後面詳細讨論 pickle。

假定您喜歡将任何事物都儲存成對象,而且希望避免将對象轉換成某種基于非對象存儲的開銷;那麼 pickle 檔案可以提供這些好處,但有時可能需要比這種簡單的 pickle 檔案更健壯以及更具有可伸縮性的事物。例如,隻用 pickle 不能解決命名和查找 pickle 檔案這樣的問題,另外,它也不能支援并發地通路持久性對象。如果需要這些方面的功能,則要求助類似于 zodb(針對 python 的 z 對象資料庫)這類資料庫。zodb 是一個健壯的、多使用者的和面向對象的資料庫系統,它能夠存儲和管理任意複雜的 python

對象,并支援事務操作和并發控制。(請參閱 參考資料,以下載下傳 zodb。)令人足夠感興趣的是,甚至 zodb 也依靠 python 的本機序列化能力,而且要有效地使用

zodb,必須充分了解 pickle。

另一種令人感興趣的解決持久性問題的方法是 prevayler,它最初是用 java 實作的(有關 prevaylor 方面的 developerworks 文章,請參閱 參考資料)。最近,一群

python 程式員将 prevayler 移植到了 python 上,另起名為 pypersyst,由 sourceforge 托管(有關至 pypersyst 項目的連結,請參閱 參考資料)。prevayler/pypersyst

概念也是建立在 java 和 python 語言的本機序列化能力之上。pypersyst 将整個對象系統儲存在記憶體中,并通過不時地将系統快照 pickle 到磁盤以及維護一個指令日志(通過此日志可以重新應用最新的快照)來提供災難恢複。是以,盡管使用 pypersyst 的應用程式受到可用記憶體的限制,但好處是本機對象系統可以完全裝入到記憶體中,因而速度極快,而且實作起來要比如 zodb 這樣的資料庫簡單,zodb 允許對象的數目比同時在能記憶體中所保持的對象要多。

既然我們已經簡要讨論了存儲持久對象的各種方法,那麼現在該詳細探讨 pickle 過程了。雖然我們主要感興趣的是探索以各種方式來儲存 python 對象,而不必将其轉換成某種其它格式,但我們仍然還有一些需要關注的地方,譬如:如何有效地 pickle 和 unpickle 簡單對象以及複雜對象,包括定制類的執行個體;如何維護對象的引用,包括循環引用和遞歸引用;以及如何處理類定義發生的變化,進而使用以前經過 pickle 的執行個體時不會發生問題。我們将在随後關于 python 的 pickle 能力探讨中涉及所有這些問題。

pickle 子產品及其同類子產品 cpickle 向 python 提供了 pickle 支援。後者是用 c 編碼的,它具有更好的性能,對于大多數應用程式,推薦使用該子產品。我們将繼續讨論 pickle ,但本文的示例實際是利用了 cpickle 。由于其中大多數示例要用

python shell 來顯示,是以先展示一下如何導入 cpickle ,并可以作為 pickle 來引用它:

>>> import cpickle as pickle

現在已經導入了該子產品,接下來讓我們看一下 pickle 接口。 pickle 子產品提供了以下函數對: dumps(object) 傳回一個字元串,它包含一個

pickle 格式的對象; loads(string) 傳回包含在 pickle 字元串中的對象; dump(object,

file) 将對象寫到檔案,這個檔案可以是實際的實體檔案,但也可以是任何類似于檔案的對象,這個對象具有 write() 方法,可以接受單個的字元串參數; load(file) 傳回包含在

pickle 檔案中的對象。

預設情況下, dumps() 和 dump() 使用可列印的

ascii 表示來建立 pickle。兩者都有一個 final 參數(可選),如果為 true ,則該參數指定用更快以及更小的二進制表示來建立 pickle。 loads() 和 load() 函數自動檢測

pickle 是二進制格式還是文本格式。

清單 1 顯示了一個互動式會話,這裡使用了剛才所描述的 dumps() 和 loads() 函數:

[Python]持久存儲

注:該文本 pickle 格式很簡單,這裡就不解釋了。事實上,在 pickle 子產品中記錄了所有使用的約定。我們還應該指出,在我們的示例中使用的都是簡單對象,是以使用二進制 pickle 格式不會在節省空間上顯示出太大的效率。然而,在實際使用複雜對象的系統中,您會看到,使用二進制格式可以在大小和速度方面帶來顯著的改進。

接下來,我們看一些示例,這些示例用到了 dump() 和 load() ,它們使用檔案和類似檔案的對象。這些函數的操作非常類似于我們剛才所看到的 dumps() 和 loads() ,差別在于它們還有另一種能力

— dump() 函數能一個接着一個地将幾個對象轉儲到同一個檔案。随後調用 load() 來以同樣的順序檢索這些對象。清單

2 顯示了這種能力的實際應用:

[Python]持久存儲

到目前為止,我們講述了關于 pickle 方面的基本知識。在這一節,将讨論一些進階問題,當您開始 pickle 複雜對象時,會遇到這些問題,其中包括定制類的執行個體。幸運的是,python 可以很容易地處理這種情形。

從空間和時間上說,pickle 是可移植的。換句話說,pickle 檔案格式獨立于機器的體系結構,這意味着,例如,可以在 linux 下建立一個 pickle,然後将它發送到在 windows 或 mac os 下運作的 python 程式。并且,當更新到更新版本的 python 時,不必擔心可能要廢棄已有的 pickle。python 開發人員已經保證 pickle 格式将可以向後相容 python 各個版本。事實上,在 pickle 子產品中提供了有關目前以及所支援的格式方面的詳細資訊:

在 python 中,變量是對象的引用。同時,也可以用多個變量引用同一個對象。經證明,python 在用經過 pickle 的對象維護這種行為方面絲毫沒有困難,如清單 4 所示:

可以将剛才示範過的對象引用支援擴充到 循環引用(兩個對象各自包含對對方的引用)和 遞歸引用(一個對象包含對其自身的引用)。下面兩個清單着重顯示這種能力。我們先看一下遞歸引用:

現在,看一個循環引用的示例:

注意,如果分别 pickle 每個對象,而不是在一個元組中一起 pickle 所有對象,會得到略微不同(但很重要)的結果,如清單 7 所示:

正如在上一個示例所暗示的,隻有在這些對象引用記憶體中同一個對象時,它們才是相同的。在 pickle 情形中,每個對象被恢複到一個與原來對象相等的對象,但不是同一個對象。換句話說,每個 pickle 都是原來對象的一個副本:

同時,我們看到 python 能夠維護對象之間的引用,這些對象是作為一個單元進行 pickle 的。然而,我們還看到分别調用 dump() 會使 python 無法維護對在該單元外部進行 pickle

的對象的引用。相反,python 複制了被引用對象,并将副本和被 pickle 的對象存儲在一起。對于 pickle 和恢複單個對象層次結構的應用程式,這是沒有問題的。但要意識到還有其它情形。

值得指出的是,有一個選項确實允許分别 pickle 對象,并維護互相之間的引用,隻要這些對象都是 pickle 到同一檔案即可。 pickle 和cpickle 子產品提供了一個 pickler (與此相對應是 unpickler ),它能夠跟蹤已經被

pickle 的對象。通過使用這個 pickler ,将會通過引用而不是通過值來 pickle 共享和循環引用:

一些對象類型是不可 pickle 的。例如,python 不能 pickle 檔案對象(或者任何帶有對檔案對象引用的對象),因為 python 在 unpickle 時不能保證它可以重建該檔案的狀态(另一個示例比較難懂,在這類文章中不值得提出來)。試圖 pickle 檔案對象會導緻以下錯誤:

與 pickle 簡單對象類型相比,pickle 類執行個體要多加留意。這主要由于 python 會 pickle 執行個體資料(通常是 _dict_ 屬性)和類的名稱,而不會 pickle 類的代碼。當

python unpickle 類的執行個體時,它會試圖使用在 pickle 該執行個體時的确切的類名稱和子產品名稱(包括任何包的路徑字首)導入包含該類定義的子產品。另外要注意,類定義必須出現在子產品的最頂層,這意味着它們不能是嵌套的類(在其它類或函數中定義的類)。

當 unpickle 類的執行個體時,通常不會再調用它們的 _init_() 方法。相反,python 建立一個通用類執行個體,并應用已進行過 pickle 的執行個體屬性,同時設定該執行個體的 _class_ 屬性,使其指向原來的類。

對 python 2.2 中引入的新型類進行 unpickle 的機制與原來的略有不同。雖然處理的結果實際上與對舊型類處理的結果相同,但 python 使用copy_reg 子產品的 _reconstructor() 函數來恢複新型類的執行個體。

如果希望對新型或舊型類的執行個體修改預設的 pickle 行為,則可以定義特殊的類的方法 _getstate_() 和 _setstate_() ,在儲存和恢複類執行個體的狀态資訊期間,python

會調用這些方法。在以下幾節中,我們會看到一些示例利用了這些特殊的方法。

現在,我們看一個簡單的類執行個體。首先,建立一個 persist.py 的 python 子產品,它包含以下新型類的定義:

現在可以 pickle foo 執行個體,并看一下它的表示:

可以看到這個類的名稱 foo 和全限定的子產品名稱 orbtech.examples.persist 都存儲在

pickle 中。如果将這個執行個體 pickle 成一個檔案,稍後再 unpickle 它或在另一台機器上 unpickle,則 python 會試圖導入 orbtech.examples.persist 子產品,如果不能導入,則會抛出異常。如果重命名該類和該子產品或者将該子產品移到另一個目錄,則也會發生類似的錯誤。

這裡有一個 python 發出錯誤消息的示例,當我們重命名 foo 類,然後試圖裝入先前進行過 pickle 的 foo 執行個體時會發生該錯誤:

在重命名 persist.py 子產品之後,也會發生類似的錯誤:

我們會在下面 模式改進這一節提供一些技術來管理這類更改,而不會破壞現有的 pickle。

前面提到對一些對象類型(譬如,檔案對象)不能進行 pickle。處理這種不能 pickle 的對象的執行個體屬性時可以使用特殊的方法( _getstate_()和 _setstate_() )來修改類執行個體的狀态。這裡有一個 foo 類的示例,我們已經對它進行了修改以處理檔案對象屬性:

pickle foo 的執行個體時,python 将隻 pickle 當它調用該執行個體的 _getstate_() 方法時傳回給它的值。類似的,在

unpickle 時,python 将提供經過 unpickle 的值作為參數傳遞給執行個體的 _setstate_() 方法。在 _setstate_() 方法内,可以根據經過

pickle 的名稱和位置資訊來重建檔案對象,并将該檔案對象配置設定給這個執行個體的 logfile 屬性。

随着時間的推移,您會發現自己必須要更改類的定義。如果已經對某個類執行個體進行了 pickle,而現在又需要更改這個類,則您可能要檢索和更新那些執行個體,以便它們能在新的類定義下繼續正常工作。而我們已經看到在對類或子產品進行某些更改時,會出現一些錯誤。幸運的是,pickle 和 unpickle 過程提供了一些 hook,我們可以用它們來支援這種模式改進的需要。

在這一節,我們将探讨一些方法來預測常見問題以及如何解決這些問題。由于不能 pickle 類執行個體代碼,是以可以添加、更改和除去方法,而不會影響現有的經過 pickle 的執行個體。出于同樣的原因,可以不必擔心類的屬性。您必須確定包含類定義的代碼子產品在 unpickle 環境中可用。同時還必須為這些可能導緻 unpickle 問題的更改做好規劃,這些更改包括:更改類名、添加或除去執行個體的屬性以及改變類定義子產品的名稱或位置。

要更改類名,而不破壞先前經過 pickle 的執行個體,請遵循以下步驟。首先,確定原來的類的定義沒有被更改,以便在 unpickle 現有執行個體時可以找到它。不要更改原來的名稱,而是在與原來類定義所在的同一個子產品中,建立該類定義的一個副本,同時給它一個新的類名。然後使用實際的新類名來替代 newclassname ,将以下方法添加到原來類的定義中:

當 unpickle 現有執行個體時,python 将查找原來類的定義,并調用執行個體的 _setstate_() 方法,同時将給新的類定義重新配置設定該執行個體的 _class_ 屬性。一旦确定所有現有的執行個體都已經

unpickle、更新和重新 pickle 後,可以從源代碼子產品中除去舊的類定義。

這些特殊的狀态方法 _getstate_() 和 _setstate_() 再一次使我們能控制每個執行個體的狀态,并使我們有機會處理執行個體屬性中的更改。讓我們看一個簡單的類的定義,我們将向其添加和除去一些屬性。這是是最初的定義:

假定已經建立并 pickle 了 person 的執行個體,現在我們決定真的隻想存儲一個名稱屬性,而不是分别存儲姓和名。這裡有一種方式可以更改類的定義,它将先前經過 pickle 的執行個體遷移到新的定義:

在這個示例,我們添加了一個新的屬性 fullname ,并除去了兩個現有的屬性 firstname 和 lastname 。當對先前進行過

pickle 的執行個體執行 unpickle 時,其先前進行過 pickle 的狀态會作為字典傳遞給 _setstate_() ,它将包括 firstname 和 lastname 屬性的值。接下來,将這兩個值組合起來,并将它們配置設定給新屬性 fullname 。在這個過程中,我們删除了狀态字典中舊的屬性。更新和重新

pickle 先前進行過 pickle 的所有執行個體之後,現在可以從類定義中除去 _setstate_() 方法。

在概念上,子產品的名稱或位置的改變類似于類名稱的改變,但處理方式卻完全不同。那是因為子產品的資訊存儲在 pickle 中,而不是通過标準的 pickle 接口就可以修改的屬性。事實上,改變子產品資訊的唯一辦法是對實際的 pickle 檔案本身執行查找和替換操作。至于如何确切地去做,這取決于具體的作業系統和可使用的工具。很顯然,在這種情況下,您會想備份您的檔案,以免發生錯誤。但這種改動應該非常簡單,并且對二進制 pickle 格式進行更改與對文本 pickle 格式進行更改應該一樣有效。