python持久性管理pickle模組詳細介紹

來源:互聯網
上載者:User
持久性就是指保持對象,甚至在多次執行同一程式之間也保持對象。通過本文,您會對 Python對象的各種持久性機制(從關聯式資料庫到 Python 的 pickle以及其它機制)有一個總體認識。另外,還會讓您更深一步地瞭解Python 的對象序列化能力。
什麼是持久性?

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

例如,可以將對象資料存放區在某種格式的文字檔中,譬如 CSV 檔案。或者可以用關聯式資料庫,譬如 Gadfly、MySQL、PostgreSQL 或者 DB2。這些檔案格式和資料庫都非常優秀,對於所有這些儲存機制,Python 都有健壯的介面。

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

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

一些經過 pickle 的 Python

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() 函數:


清單 1. dumps() 和 loads() 的示範

Welcome To PyCrust 0.7.2 - The Flakiest Python ShellSponsored by Orbtech - Your source for Python programming expertise.Python 2.2.1 (#1, Aug 27 2002, 10:22:32)[GCC 3.2 (Mandrake Linux 9.0 3.2-1mdk)] on linux-i386Type "copyright", "credits" or "license" for more information.>>> import cPickle as pickle>>> t1 = ('this is a string', 42, [1, 2, 3], None)>>> t1('this is a string', 42, [1, 2, 3], None)>>> p1 = pickle.dumps(t1)>>> p1"(S'this is a string'\nI42\n(lp1\nI1\naI2\naI3\naNtp2\n.">>> print p1(S'this is a string'I42(lp1I1aI2aI3aNtp2.>>> t2 = pickle.loads(p1)>>> t2('this is a string', 42, [1, 2, 3], None)>>> p2 = pickle.dumps(t1, True)>>> p2'(U\x10this is a stringK*]q\x01(K\x01K\x02K\x03eNtq\x02.'>>> t3 = pickle.loads(p2)>>> t3('this is a string', 42, [1, 2, 3], None)

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

接下來,我們看一些樣本,這些樣本用到了 dump() 和 load() ,它們使用檔案和類似檔案的對象。這些函數的操作非常類似於我們剛才所看到的 dumps() 和 loads() ,區別在於它們還有另一種能力 — dump() 函數能一個接著一個地將幾個對象轉儲到同一個檔案。隨後調用 load() 來以同樣的順序檢索這些對象。清單 2 顯示了這種能力的實際應用:

清單 2. dump() 和 load() 樣本

>>> a1 = 'apple'>>> b1 = {1: 'One', 2: 'Two', 3: 'Three'}>>> c1 = ['fee', 'fie', 'foe', 'fum']>>> f1 = file('temp.pkl', 'wb')>>> pickle.dump(a1, f1, True)>>> pickle.dump(b1, f1, True)>>> pickle.dump(c1, f1, True)>>> f1.close()>>> f2 = file('temp.pkl', 'rb')>>> a2 = pickle.load(f2)>>> a2'apple'>>> b2 = pickle.load(f2)>>> b2{1: 'One', 2: 'Two', 3: 'Three'}>>> c2 = pickle.load(f2)>>> c2['fee', 'fie', 'foe', 'fum']>>> f2.close()

Pickle 的威力

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

可移植性

從 空間和時間上說,Pickle 是可移植的。換句話說,pickle 檔案格式獨立於機器的體繫結構,這意味著,例如,可以在 Linux 下建立一個 pickle,然後將它發送到在 Windows 或 Mac OS 下啟動並執行 Python 程式。並且,當升級到更新版本的 Python 時,不必擔心可能要廢棄已有的 pickle。Python 開發人員已經保證 pickle 格式將可以向後相容 Python 各個版本。事實上,在 pickle 模組中提供了有關目前以及所支援的格式方面的詳細資料:


清單 3. 檢索所支援的格式

>>> pickle.format_version'1.3'>>> pickle.compatible_formats['1.0', '1.1', '1.2']

多個引用,同一對象

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

清單 4. 對象引用的維護

>>> a = [1, 2, 3]>>> b = a>>> a[1, 2, 3]>>> b[1, 2, 3]>>> a.append(4)>>> a[1, 2, 3, 4]>>> b[1, 2, 3, 4]>>> c = pickle.dumps((a, b))>>> d, e = pickle.loads(c)>>> d[1, 2, 3, 4]>>> e[1, 2, 3, 4]>>> d.append(5)>>> d[1, 2, 3, 4, 5]>>> e[1, 2, 3, 4, 5]

循環參考和遞迴引用

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

>清單 5. 遞迴引用

>>> l = [1, 2, 3]>>> l.append(l)>>> l[1, 2, 3, [...]]>>> l[3][1, 2, 3, [...]]>>> l[3][3][1, 2, 3, [...]]>>> p = pickle.dumps(l)>>> l2 = pickle.loads(p)>>> l2[1, 2, 3, [...]]>>> l2[3][1, 2, 3, [...]]>>> l2[3][3][1, 2, 3, [...]]

現在,看一個循環參考的樣本:

清單 6. 循環參考

>>> a = [1, 2]>>> b = [3, 4]>>> a.append(b)>>> a[1, 2, [3, 4]]>>> b.append(a)>>> a[1, 2, [3, 4, [...]]]>>> b[3, 4, [1, 2, [...]]]>>> a[2][3, 4, [1, 2, [...]]]>>> b[2][1, 2, [3, 4, [...]]]>>> a[2] is b>>> b[2] is a>>> f = file('temp.pkl', 'w')>>> pickle.dump((a, b), f)>>> f.close()>>> f = file('temp.pkl', 'r')>>> c, d = pickle.load(f)>>> f.close()>>> c[1, 2, [3, 4, [...]]]>>> d[3, 4, [1, 2, [...]]]>>> c[2][3, 4, [1, 2, [...]]]>>> d[2][1, 2, [3, 4, [...]]]>>> c[2] is d>>> d[2] is c

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


清單 7. 分別 pickle vs. 在一個元組中一起 pickle

>>> f = file('temp.pkl', 'w')>>> pickle.dump(a, f)>>> pickle.dump(b, f)>>> f.close()>>> f = file('temp.pkl', 'r')>>> c = pickle.load(f)>>> d = pickle.load(f)>>> f.close()>>> c[1, 2, [3, 4, [...]]]>>> d[3, 4, [1, 2, [...]]]>>> c[2][3, 4, [1, 2, [...]]]>>> d[2][1, 2, [3, 4, [...]]]>>> c[2] is d>>> d[2] is c

相等,但並不總是相同

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


清單 8. 作為原來對象副本的被恢複的對象

>>> j = [1, 2, 3]>>> k = j>>> k is j>>> x = pickle.dumps(k)>>> y = pickle.loads(x)>>> y[1, 2, 3]>>> y == k>>> y is k>>> y is j>>> k is j

同時,我們看到 Python 能夠維護對象之間的引用,這些對象是作為一個單元進行 pickle 的。然而,我們還看到分別調用 dump() 會使 Python 無法維護對在該單元外部進行 pickle 的對象的引用。相反,Python 複製了被引用對象,並將副本和被 pickle 的Object Storage Service在一起。對於 pickle 和恢複單個對象階層的應用程式,這是沒有問題的。但要意識到還有其它情形。

值得指出的是,有一個選項確實允許分別 pickle 對象,並維護相互之間的引用,只要這些對象都是 pickle 到同一檔案即可。 pickle 和 cPickle 模組提供了一個 Pickler (與此相對應是 Unpickler ),它能夠跟蹤已經被 pickle 的對象。通過使用這個 Pickler ,將會通過引用而不是通過值來 pickle 共用和循環參考:


清單 9. 維護分別 pickle 的對象間的引用

>>> f = file('temp.pkl', 'w')>>> pickler = pickle.Pickler(f)>>> pickler.dump(a)<cPickle.Pickler object at 0x89b0bb8>>>> pickler.dump(b)<cPickle.Pickler object at 0x89b0bb8>>>> f.close()>>> f = file('temp.pkl', 'r')>>> unpickler = pickle.Unpickler(f)>>> c = unpickler.load()>>> d = unpickler.load()>>> c[2][3, 4, [1, 2, [...]]]>>> d[2][1, 2, [3, 4, [...]]]>>> c[2] is d>>> d[2] is c

不可 pickle 的對象

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


清單 10. 試圖 pickle 檔案對象的結果

>>> f = file('temp.pkl', 'w')>>> p = pickle.dumps(f)Traceback (most recent call last):  File "<input>", line 1, in ?  File "/usr/lib/python2.2/copy_reg.py", line 57, in _reduce    raise TypeError, "can't pickle %s objects" % base.__name__TypeError: can't pickle file objects

類執行個體

與 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 模組,它包含以下新型類的定義:

清單 11. 新型類的定義

class Foo(object):    def __init__(self, value):        self.value = value

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


清單 12. pickle Foo 執行個體

>>> import cPickle as pickle>>> from Orbtech.examples.persist import Foo>>> foo = Foo('What is a Foo?')>>> p = pickle.dumps(foo)>>> print pccopy_reg_reconstructorp1(cOrbtech.examples.persistFoop2c__builtin__objectp3NtRp4(dp5S'value'p6S'What is a Foo?'sb.>>>

可以看到這個類的名稱 Foo 和全限定的模組名稱 Orbtech.examples.persist 都儲存在 pickle 中。如果將這個執行個體 pickle 成一個檔案,稍後再 unpickle 它或在另一台機器上 unpickle,則 Python 會試圖匯入 Orbtech.examples.persist 模組,如果不能匯入,則會拋出異常。如果重新命名該類和該模組或者將該模組移到另一個目錄,則也會發生類似的錯誤。

這裡有一個 Python 發出錯誤訊息的樣本,當我們重新命名 Foo 類,然後試圖裝入先前進行過 pickle 的 Foo 執行個體時會發生該錯誤:


清單 13. 試圖裝入一個被重新命名的 Foo 類的經過 pickle 的執行個體

>>> import cPickle as pickle>>> f = file('temp.pkl', 'r')>>> foo = pickle.load(f)Traceback (most recent call last):  File "<input>", line 1, in ?AttributeError: 'module' object has no attribute 'Foo'

在重新命名 persist.py 模組之後,也會發生類似的錯誤:

清單 14. 試圖裝入一個被重新命名的 persist.py 模組的經過 pickle 的執行個體

>>> import cPickle as pickle>>> f = file('temp.pkl', 'r')>>> foo = pickle.load(f)Traceback (most recent call last):  File "<input>", line 1, in ?ImportError: No module named persist

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

特殊的狀態方法

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

清單 15. 處理不能 pickle 的執行個體屬性

class Foo(object):    def __init__(self, value, filename):        self.value = value        self.logfile = file(filename, 'w')    def __getstate__(self):        """Return state values to be pickled."""        f = self.logfile        return (self.value, f.name, f.tell())    def __setstate__(self, state):        """Restore state from the unpickled state values."""        self.value, name, position = state        f = file(name, 'w')        f.seek(position)        self.logfile = f

模式改進

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

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

類名的更改

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

清單 16. 更改類名:添加到原來類定義的方法

def __setstate__(self, state):    self.__dict__.update(state)    self.__class__ = NewClassName

當 unpickle 現有執行個體時,Python 將尋找原來類的定義,並調用執行個體的 _setstate_() 方法,同時將給新的類定義重新分配該執行個體的 _class_ 屬性。一旦確定所有現有的執行個體都已經 unpickle、更新和重新 pickle 後,可以從原始碼模組中除去舊的類定義。

屬性的添加和刪除

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


清單 17. 最初的類定義

class Person(object):    def __init__(self, firstname, lastname):        self.firstname = firstname        self.lastname = lastname

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

class Person(object):    def __init__(self, fullname):        self.fullname = fullname    def __setstate__(self, state):        if 'fullname' not in state:            first = ''            last = ''            if 'firstname' in state:                first = state['firstname']                del state['firstname']            if 'lastname' in state:                last = state['lastname']                del state['lastname']            self.fullname = " ".join([first, last]).strip()        self.__dict__.update(state)

模組的修改

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

結束語

對象持久性依賴於底層程式設計語言的對象序列化能力。對於 Python 對象即意味著 pickle。Python 的 pickle 為 Python 對象有效持久性管理提供了健壯的和可靠的基礎。在下面的 參考資料中,您將會找到有關建立在 Python pickle 能力之上的系統的資訊。




聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.