지속성이란 동일한 프로그램을 여러 번 실행하는 사이에도 개체를 유지하는 것을 의미합니다. 이 기사를 통해 관계형 데이터베이스부터 Python의 피클 및 기타 메커니즘에 이르기까지 Python 개체의 다양한 지속성 메커니즘에 대한 개요를 얻을 수 있습니다. 또한 Python의 객체 직렬화 기능에 대한 더 깊은 이해도 제공합니다.
끈기란 무엇인가요?
끈기의 기본 개념은 간단합니다. 매일 할 일 항목을 관리하는 프로그램인 Python 프로그램이 있고 프로그램 실행 사이에 애플리케이션 개체(할 일 항목)를 저장하려고 한다고 가정합니다. 즉, 나중에 검색할 수 있도록 객체를 디스크에 저장하려고 합니다. 이것이 바로 끈기입니다. 이를 달성하는 방법에는 여러 가지가 있으며 각각 장단점이 있습니다.
예를 들어, 객체 데이터는 CSV 파일과 같은 일부 형식의 텍스트 파일에 저장할 수 있습니다. 또는 Gadfly, MySQL, PostgreSQL 또는 DB2와 같은 관계형 데이터베이스를 사용할 수 있습니다. 이러한 파일 형식과 데이터베이스는 훌륭하며 Python은 이러한 모든 저장 메커니즘에 대한 강력한 인터페이스를 갖추고 있습니다.
이러한 저장 메커니즘에는 모두 한 가지 공통점이 있습니다. 저장된 데이터는 이러한 데이터에서 작동하는 개체 및 프로그램과 독립적입니다. 이것의 장점은 데이터를 다른 응용 프로그램이 사용할 수 있는 공유 리소스로 사용할 수 있다는 것입니다. 단점은 이런 방식으로 다른 프로그램이 객체의 데이터에 액세스하도록 허용할 수 있다는 것입니다. 이는 객체 지향 캡슐화 원칙을 위반합니다. 즉, 객체의 데이터는 객체 자체의 공용 인터페이스를 통해서만 액세스할 수 있습니다.
또한 일부 애플리케이션의 경우 관계형 데이터베이스 접근 방식이 적합하지 않을 수 있습니다. 특히 관계형 데이터베이스는 객체를 이해하지 못합니다. 대조적으로, 관계형 데이터베이스는 자체 유형 시스템과 관계형 데이터 모델(테이블)을 적용하며, 각 테이블은 일련의 튜플(행)을 포함하고 각 행은 고정된 수의 정적으로 유형이 지정된 필드(열)를 포함합니다. 응용 프로그램의 개체 모델을 관계형 모델로 쉽게 변환할 수 없는 경우 개체를 튜플에 매핑하고 튜플을 다시 개체에 매핑하는 데 어려움을 겪게 됩니다. 이러한 어려움을 종종 임피던스 불일치 문제라고 합니다.
일부 피클 Python
pickle 모듈과 그 사촌인 cPickle은 Python에 피클 지원을 제공합니다. 후자는 C로 코딩되어 있으며 성능이 더 좋고 대부분의 응용 프로그램에 이 모듈을 권장합니다. 계속해서 피클에 대해 논의하겠지만 이 기사의 예제에서는 실제로 cPickle을 활용합니다. 이러한 예제의 대부분은 Python 셸을 사용하여 표시되므로 먼저 cPickle을 가져오고 이를 피클로 참조하는 방법을 살펴보겠습니다.
>>> import cPickle as pickle
이제 모듈을 가져왔으므로 피클 인터페이스를 살펴보겠습니다. . pickle 모듈은 다음과 같은 함수 쌍을 제공합니다: dump(object)는 피클 형식으로 객체를 포함하는 문자열을 반환하고, load(string)은 피클 문자열에 포함된 객체를 반환합니다. dump(object, file)는 객체를 파일에 씁니다. file은 실제 물리적 파일일 수도 있지만 파일과 유사한 객체일 수도 있습니다. 이 객체에는 단일 문자열 매개변수를 허용하는 write() 메서드가 있습니다. load(file)은 피클 파일에 포함된 객체를 반환합니다.
기본적으로 dump() 및 dump()는 인쇄 가능한 ASCII 표현을 사용하여 피클을 만듭니다. 둘 다 True인 경우 피클이 더 빠르고 더 작은 이진 표현으로 생성되도록 지정하는 선택적 최종 매개변수를 사용합니다. load() 및 load() 함수는 피클이 바이너리 형식인지 텍스트 형식인지 자동으로 감지합니다.
목록 1은 방금 설명한 dump() 및 load() 함수를 사용하는 대화형 세션을 보여줍니다.
목록 1. dump() 및 load() 데모
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)
참고: 텍스트 피클 형식은 매우 간단하므로 여기서는 설명하지 않습니다. 실제로 사용된 모든 규칙은 피클 모듈에 문서화되어 있습니다. 또한 예제에서는 간단한 개체를 사용하고 있으므로 바이너리 피클 형식을 사용하면 공간 절약 측면에서 효율성이 크게 나타나지 않는다는 점도 지적해야 합니다. 그러나 복잡한 객체를 사용하는 실제 시스템에서는 바이너리 형식을 사용하면 크기와 속도가 크게 향상될 수 있음을 알 수 있습니다.
다음으로 파일 및 파일 유사 객체에 대해 작동하는 dump() 및 load() 를 사용하는 몇 가지 예를 살펴보겠습니다. 이러한 함수의 작동은 방금 본 dump() 및 load()와 매우 유사합니다. 단, 다른 기능이 있습니다. 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의 강력한 기능
지금까지 우리는 피클에 대한 기본. 이 섹션에서는 사용자 정의 클래스의 인스턴스를 포함하여 복잡한 객체를 피클링하기 시작할 때 직면하게 되는 몇 가지 고급 문제에 대해 논의합니다. 다행스럽게도 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('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 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, '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 객체의 경우 이는 피클을 의미합니다. Python의 피클은 Python 객체의 효율적인 지속성 관리를 위한 강력하고 안정적인 기반을 제공합니다. 아래 리소스에서는 Python의 피클 기능을 기반으로 구축된 시스템에 대한 정보를 찾을 수 있습니다.
Python 지속성 관리 피클 모듈에 대한 자세한 소개 및 관련 기사를 보려면 결제하세요. PHP 중국어 웹사이트에 주목하세요!