红联Linux门户
Linux帮助

Linux编程之序列化存储Python对象(下)

发布时间:2006-11-02 09:31:21来源:红联作者:lgms2008
  相等,但并不总是相同

  正如在上一个示例所暗示的,只有在这些对象引用内存中同一个对象时,它们才是相同的。在 pickle 情形中,每个对象被恢复到一个与原来对象相等的对象,但不是同一个对象。换句话说,每个 pickle 都是原来对象的一个副本:

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

  清单 8. 作为原来对象副本的被恢复的对象

  同时,我们看到 Python 能够维护对象之间的引用,这些对象是作为一个单元进行 pickle 的。然而,我们还看到分别调用 dump() 会使 Python 无法维护对在该单元外部进行 pickle 的对象的引用。相反,Python 复制了被引用对象,并将副本和被 pickle 的对象存储在一起。对于 pickle 和恢复单个对象层次结构的应用程序,这是没有问题的。但要意识到还有其它情形。

  值得指出的是,有一个选项确实允许分别 pickle 对象,并维护相互之间的引用,只要这些对象都是 pickle 到同一文件即可。pickle 和 cPickle 模块提供了一个 Pickler(与此相对应是 Unpickler),它能够跟踪已经被 pickle 的对象。通过使用这个 Pickler,将会通过引用而不是通过值来 pickle 共享和循环引用:

  >>> f = file('temp.pkl', 'w')
  >>> pickler = pickle.Pickler(f)
  >>> pickler.dump(a)
  
  >>> pickler.dump(b)
  
  >>> 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
  1
  >>> d[2] is c
  1

  清单 9. 维护分别 pickle 的对象间的引用

  不可 pickle 的对象

  一些对象类型是不可 pickle 的。例如,Python 不能 pickle 文件对象(或者任何带有对文件对象引用的对象),因为 Python 在 unpickle 时不能保证它可以重建该文件的状态(另一个示例比较难懂,在这类文章中不值得提出来)。试图 pickle 文件对象会导致以下错误:

  >>> f = file('temp.pkl', 'w')
  >>> p = pickle.dumps(f)
  Traceback (most recent call last):
  File "", 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

  清单 10. 试图 pickle 文件对象的结果
文章评论

共有 3 条评论

  1. lgms2008 于 2006-11-02 09:33:58发表:

      类名的更改

      要更改类名,而不破坏先前经过 pickle 的实例,请遵循以下步骤。首先,确保原来的类的定义没有被更改,以便在 unpickle 现有实例时可以找到它。不要更改原来的名称,而是在与原来类定义所在的同一个模块中,创建该类定义的一个副本,同时给它一个新的类名。然后使用实际的新类名来替代 NewClassName,将以下方法添加到原来类的定义中:

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

      清单 16. 更改类名:添加到原来类定义的方法

      当 unpickle 现有实例时,Python 将查找原来类的定义,并调用实例的 _setstate_() 方法,同时将给新的类定义重新分配该实例的 _class_ 属性。一旦确定所有现有的实例都已经 unpickle、更新和重新 pickle 后,可以从源代码模块中除去旧的类定义。

      属性的添加和删除

      这些特殊的状态方法 _getstate_() 和 _setstate_() 再一次使我们能控制每个实例的状态,并使我们有机会处理实例属性中的更改。让我们看一个简单的类的定义,我们将向其添加和除去一些属性。这是是最初的定义:

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

      清单 17. 最初的类定义

      假定已经创建并 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)

      清单 18. 新的类定义

      在这个示例,我们添加了一个新的属性 fullname,并除去了两个现有的属性 firstname 和 lastname。当对先前进行过 pickle 的实例执行 unpickle 时,其先前进行过 pickle 的状态会作为字典传递给 _setstate_(),它将包括 firstname 和 lastname 属性的值。接下来,将这两个值组合起来,并将它们分配给新属性 fullname。在这个过程中,我们删除了状态字典中旧的属性。更新和重新 pickle 先前进行过 pickle 的所有实例之后,现在可以从类定义中除去 _setstate_() 方法。

      模块的修改

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

      结束语

      对象持久性依赖于底层编程语言的对象序列化能力。对于 Python 对象即意味着 pickle。Python 的 pickle 为 Python 对象有效的持久性管理提供了健壮的和可靠的基础。在下面的参考资料中,您将会找到有关建立在 Python pickle 能力之上的系统的信息。

  2. lgms2008 于 2006-11-02 09:32:56发表:

      特殊的状态方法

      前面提到对一些对象类型(譬如,文件对象)不能进行 pickle。处理这种不能 pickle 的对象的实例属性时可以使用特殊的方法(_getstate_() 和 _setstate_())来修改类实例的状态。这里有一个 Foo 类的示例,我们已经对它进行了修改以处理文件对象属性:

      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 

      清单 15. 处理不能 pickle 的实例属性

      pickle Foo 的实例时,Python 将只 pickle 当它调用该实例的 _getstate_() 方法时返回给它的值。类似的,在 unpickle 时,Python 将提供经过 unpickle 的值作为参数传递给实例的 _setstate_() 方法。在 _setstate_() 方法内,可以根据经过 pickle 的名称和位置信息来重建文件对象,并将该文件对象分配给这个实例的 logfile 属性。

      模式改进

      随着时间的推移,您会发现自己必须要更改类的定义。如果已经对某个类实例进行了 pickle,而现在又需要更改这个类,则您可能要检索和更新那些实例,以便它们能在新的类定义下继续正常工作。而我们已经看到在对类或模块进行某些更改时,会出现一些错误。幸运的是,pickle 和 unpickle 过程提供了一些 hook,我们可以用它们来支持这种模式改进的需要。

      在这一节,我们将探讨一些方法来预测常见问题以及如何解决这些问题。由于不能 pickle 类实例代码,因此可以添加、更改和除去方法,而不会影响现有的经过 pickle 的实例。出于同样的原因,可以不必担心类的属性。您必须确保包含类定义的代码模块在 unpickle 环境中可用。同时还必须为这些可能导致 unpickle 问题的更改做好规划,这些更改包括:更改类名、添加或除去实例的属性以及改变类定义模块的名称或位置。

  3. lgms2008 于 2006-11-02 09:32:17发表:

      类实例

      与 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 模块,它包含以下新型类的定义:

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

      清单 11. 新型类的定义

      现在可以 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.
      >>>

      清单 12. pickle Foo 实例

      可以看到这个类的名称 Foo 和全限定的模块名称 Orbtech.examples.persist 都存储在 pickle 中。如果将这个实例 pickle 成一个文件,稍后再 unpickle 它或在另一台机器上 unpickle,则 Python 会试图导入 Orbtech.examples.persist 模块,如果不能导入,则会抛出异常。如果重命名该类和该模块或者将该模块移到另一个目录,则也会发生类似的错误。

      这里有一个 Python 发出错误消息的示例,当我们重命名 Foo 类,然后试图装入先前进行过 pickle 的 Foo 实例时会发生该错误:

      >>> 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'

      清单 13. 试图装入一个被重命名的 Foo 类的经过 pickle 的实例

      在重命名 persist.py 模块之后,也会发生类似的错误:

      >>> 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

      清单 14. 试图装入一个被重命名的 persist.py 模块的经过 pickle 的实例

      我们会在下面模式改进这一节提供一些技术来管理这类更改,而不会破坏现有的 pickle。