首頁 > 後端開發 > Python教學 > python持久性管理pickle模組詳細介紹

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

高洛峰
發布: 2016-12-16 11:47:29
原創
1240 人瀏覽過

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

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

例如,可以將物件資料儲存在某種格式的文字檔案中,譬如 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 Shell
Sponsored 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-i386
Type "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
(lp1
I1
aI2
aI3
aNtp2
.
>>> 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)
登入後複製

註:該文本picklekle格式很簡單,這裡就不解釋了。事實上,在 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 的对象存储在一起。对于 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(&#39;temp.pkl&#39;, &#39;r&#39;)
>>> 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(&#39;temp.pkl&#39;, &#39;w&#39;)
>>> 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&#39;t pickle %s objects" % base.__name__
TypeError: can&#39;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 p
ccopy_reg
_reconstructor
p1
(cOrbtech.examples.persist
Foo
p2
c__builtin__
object
p3
NtRp4
(dp5
S'value'
p6
S'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 "", 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 "", 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, &#39;w&#39;)
    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, &#39;w&#39;)
        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 &#39;fullname&#39; not in state:
            first = &#39;&#39;
            last = &#39;&#39;
            if &#39;firstname&#39; in state:
                first = state[&#39;firstname&#39;]
                del state[&#39;firstname&#39;]
            if &#39;lastname&#39; in state:
                last = state[&#39;lastname&#39;]
                del state[&#39;lastname&#39;]
            self.fullname = " ".join([first, last]).strip()
        self.__dict__.update(state)
登入後複製

模块的修改

在概念上,模块的名称或位置的改变类似于类名称的改变,但处理方式却完全不同。那是因为模块的信息存储在 pickle 中,而不是通过标准的 pickle 接口就可以修改的属性。事实上,改变模块信息的唯一办法是对实际的 pickle 文件本身执行查找和替换操作。至于如何确切地去做,这取决于具体的操作系统和可使用的工具。很显然,在这种情况下,您会想备份您的文件,以免发生错误。但 这种改动应该非常简单,并且对二进制 pickle 格式进行更改与对文本 pickle 格式进行更改应该一样有效。

结束语

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




更多python持久性管理pickle模組詳細介紹相關文章請關注PHP中文網!


相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板