- 31 октября 2002 г.
Управление персистентностью Python
В статье рубрики рассказывается о сериализации объектов Python, или
Питоновском консервировании объектов, - очень интересной реализации концепции
персистентности.
Используйте сериализацию для хранения объектов Python
Что такое персистентность?
Идея, лежащая в основе персистентности, довольно проста. Предположим, что у вас есть приложение Python для управления списком задач на каждый день, и вы хотите запоминать объекты приложения (свои отдельные задачи) между использованием этой программы. Другими словами, вы желаете сохранять свои объекты на жестком диске, а затем их извлекать. Это и есть персистентность. Чтобы выполнить эту задачу, у вас есть несколько возможностей, каждая из них обладает своими достоинствами и недостатками.
Например, вы могли бы хранить данные своего объекта в какой-нибудь разновидности форматированного текстового файла, как CSV-файл. Или вы могли бы использовать реляционную базу данных, такую как Gadfly, MySQL, PostgreSQL или DB2. Эти файловые форматы хорошо определены, а Python обладает надежными интерфейсами для всех этих механизмов хранения.
Общая особенность этих механизмов хранения состоит в том, что данные хранятся независимо от объектов и программ, которые работают с этими данными. Преимущество в этом случае заключается в том, что данные становятся доступными для других приложений как общий ресурс. Недостаток же в том, чтобы разрешить доступ к данным таким способом, вы нарушаете объектно-ориентированный принцип инкапсуляции, при котором данные объекта могут быть доступны только через его собственный открытый интерфейс.
Тем самым, для некоторых приложений применение реляционных баз данных, возможно, не является идеальным. В частности, из-за того, что реляционные базы данных не понимают объекты. Наоборот, они навязывают свои собственные системы типов и свои собственные модели данных для отношений (таблиц), каждая из которых содержит набор записей (рядов), состоящих из фиксированного числа статически типизированных полей (столбцов). Если объектная модель для вашего приложения не преобразовывается легко в реляционную модель, у вас будут определенные сложности при преобразовании своего объекта в записи и обратно. Эту проблему часто именуют несогласованностью интерфейсов (impedence-mismatch problem).
Персистентность объектов
Если вы хотите прозрачно сохранять объекты Python, не теряя их тождественность (identity), тип и так далее, вам потребуется некоторая форма сериализации - процесса, который превращает произвольно сложные объекты в текстовое или бинарное представление этих объектов. Таким же образом вы должны быть в состоянии восстановить эту сериализованную форму объекта обратно в объект, такой же, как и оригинал. На Python процесс сериализации называется консервированием (pickling), и вы можете законсервировать/восстановить свои объекты в/из строки, файла на диске или любого объекта, подобного файлу. Ниже мы детально рассмотрим консервирование.
Предположим, что вам нравится идея хранить все как объект, избегая накладных расходов на преобразование объектов в какую-либо разновидность хранения, не основанную на объектах. Файлы консервированных объектов (pickle files) дают такую возможность, но иногда вам потребуется что-нибудь более надежное и расширяемое, чем просто эти файлы. Например, само по себе консервирование не решает проблему наименования и обнаружения файлов консервированных объектов, как и не поддерживает одновременный доступ к перманентным объектам (persistent objects). За такими свойствами обратитесь к чему-нибудь вроде ZOBD, объектной базе данных Z для Python. ZOBD - это надежная многопользовательская объектно-ориентированная система баз данных, способная хранить и управлять произвольно сложными объектами Python, включая поддержку транзакций и управление параллельным доступом (чтобы скачать ZOBD, см. Ресурсы). Весьма интересно, что даже ZOBD полагается на Питоновские встроенные возможности сериализации, и, чтобы эффективно использовать ZOBD, у вас должно быть полное понимание консервирования.
Другой интересный подход к решению проблемы персистентности, первоначально реализованный на Java, называется Prevayler (ссылку на статью о Prevayler, опубликованную на developerWorks, см. в Ресурсах). Недавно группа программистов Python перенесла Prevayler на Python, и итог их трудов под названием PyPerSyst находится на SourceForge (ссылка на этот проект приведена в Ресурсах). Концепция Prevayler/PyPerSyst также строится на встроенных возможностях сериализации языков Java и Python. PyPerSyst держит все систему объектов в памяти и обеспечивает восстановление системы после аварии, время от времени консервируя на диск мгновенное состояние (snapshot) системы и поддерживая лог команд, которые могут повторно применяться к последнему снимку. Но, несмотря на то, что приложения, использующие PyPerSyst, по этой причине ограничены доступной памятью (RAM), преимущество состоит в том, что полностью загруженная в память система объектов, "родных" для языка, является чрезвычайно быстродействующей, и ее гораздо легче реализовать, чем ту, которая, подобно ZOBD, разрешает больше объектов, чем можно одновременно держать в памяти.
После того, как мы кратко коснулись различных способов хранения перманентных объектов, давайте детально рассмотрим процесс консервирования. Хотя основной интерес состоит в нахождении путей поддержания объектов Python без обязательного преобразования их в какой-нибудь другой формат, многие проблемы по-прежнему остались неразрешенными: как эффективно законсервировать и восстановить как простые, так и сложные объекты, включая экземпляры классов, определенных пользователем (custom classes); как поддерживать ссылки на объекты, в том числе циклические и рекурсивные; и как управлять изменениями описания класса, не создавая проблем с ранее законсервированными экземплярами. Все эти вопросы будут освещены ниже при рассмотрении Питоновских возможностей сериализации.
Суп из консервированного Python
Поддержка Питоновского консервирования проистекает из модуля pickle и его "собрата" сPickle. Второй модуль был написан на C, чтобы обеспечить лучшую производительность, и рекомендован для большинства приложений. Мы продолжим обсуждение pickle, но на самом деле в примерах будет использоваться сPickle. Поскольку большинство примеров будет показано из оболочки Python, давайте начнем с демонстрации того, как импортировать сPickle, сохраняя возможность ссылаться на него как pickle:
>>> import cPickle as pickle
После того, как мы импортировали этот модуль, давайте посмотрим на интерфейс модуля pickle. Модуль pickle предоставляет следующие пары функций: dumps(object) возвращает строку, содержащую объект в формате консервирования; loads(string) возвращает объект, находящийся в строке консервирования; dump(object, file) записывает объект в файл, который может быть как действительно физическим файлом, так и любым подобным файлу объектом, имеющим метод write(), который принимает один строчный аргумент; load(file) возвращает объект, содержащийся в файле консервированного объекта.
По умолчанию dumps() и dump() создают консервированные объекты, используя печатаемое представление ASCII. Обе функции имеют конечный факультативный аргумент, который, если True, устанавливает, что консервированные объекты будут создаваться с использованием более быстрого и меньшего по размеру бинарного представления. Функции loads() и load() автоматически определяют, находится ли консервированный объект в бинарном или текстовом формате.
Листинг 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)
Заметьте, что расшифровать текстовый формат консервированного объекта (text pickle format) не слишком сложно. Действительно, все задействованные условные обозначения документированы в модуле pickle. Также следует отметить, что для простых объектов, использующихся в нашем примере, использование бинарного формата консервированного объекта (binary pickle format) не привело к большому выигрышу в размере. Однако, в реальной системе со сложными объектами, при использовании бинарного формата вы получите заметное улучшение в размере и скорости.
Далее мы рассмотрим некоторые примеры, в которых 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()
Мощь консервированных объектов
До сих пор освещались основы консервирования. В этом разделе мы рассмотрим некоторые нетривиальные проблемы, которые возникают при консервировании сложных объектов, включая экземпляры классов, определяемых пользователем. К счастью, Python, как вы увидите, довольно легко расправляется с такими задачами.
Переносимость
Консервированные объекты переносимы через пространство и время. Другими
словами, формат файла консервированного объекта не зависит от архитектуры
машины, что означает, что вы можете создать консервированный объект, например,
под Linux и отправить его в программу Python, исполняемую под Windows и Mac OS.
А если вы перейдете на более новую версию Python, вам не нужно беспокоиться о
том, что вы, возможно, потеряете существующие консервированные объекты.
Разработчики Python предусмотрели, что формат консервированного объекта будет
совместим с более ранними версиями Python. К тому же подробная информация о
текущем и поддерживаемом форматах предоставляется с модулем
pickle:
Листинг 3. Получение информации о поддерживаемых форматах
>>> pickle.format_version
'1.3'
>>> pickle.compatible_formats
['1.0', '1.1', '1.2']
Многочисленные ссылки, один и тот же объект
Переменная на Python- это ссылка на объект. Вы можете иметь многочисленные
переменные, ссылающиеся на один и тот же объект. Оказывается, что Python не
испытывает никаких сложностей при поддержании этого поведения и с
консервированными объектами, как следует из Листинга 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
1
>>> b[2] is a
1
>>> 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
1
>>> d[2] is c
1
Заметьте, что мы получаем хоть и неприметно, но существенно различные
результаты, если мы консервируем каждый объект отдельно, а не совместно внутри
записи, как показано в Листинге 7:
Листинг 7. Консервирование по отдельности по сравнению с совместным
консервированием внутри записи
>>> 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
0
>>> d[2] is c
0
Эквивалентны, но не всегда идентичны
С помощью последнего примера мы дали понять, что объекты идентичны, только
если они ссылаются на один и тот же объект в памяти. В случае консервированных
объектов, каждый из них восстанавливается в объект, который эквивалентен
оригиналу, но не идентичен. Другими словами, каждый консервированный объект -
это копия оригинального объекта.
Листинг 8. Восстановленные объекты как копии оригиналов
>>> 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
В то же время мы видели, что Python способен поддерживать ссылки между объектами, которые консервируются как блок (unit). Однако, мы также узнали, что раздельные вызовы dump() лишают Python способности поддерживать ссылки на объекты вне консервируемого блока. Python, наоборот, создает копию объекта, на который ссылаются, и хранит его с консервируемым элементом (item). Для приложения, которое консервирует и восстанавливает иерархию одиночного объекта, это не является проблемой. Но это то, о чем не стоит забывать в других ситуациях.
Также нужно отметить возможность, позволяющую консервированным объектам
поддерживать ссылки друг на друга, если все они законсервированы в один и тот
же файл. Модули pickle и cPickle предоставляют объекты
Pickler (и соответствующий Unpickler), которые могут
отслеживать (track) объекты, которые уже были законсервированы. При
использовании Pickler, разделяемые и циклические ссылки будут
законсервированы по ссылке, а не по значению:
Листинг 9. Поддержание ссылок среди отдельно консервированных
объектов
>>> 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
1
>>> d[2] is c
1
Неконсервируемые объекты
Некоторые типы объектов не могут быть законсервированы. Например, Python не
может законсервировать файловый объект (или любой объект с ссылкой на файловый
объект), потому что Python не может гарантировать, что он воссоздаст состояние
файла при восстановлении. (Другие примеры настолько незначительны, что в данной
статье о них не стоит упоминать.) Попытка законсервировать файловый объект
приведет к следующей ошибке:
Листинг 10, Результат попытки законсервировать файловый
объект
>>> 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
Экземпляры класса
Консервирование экземпляров класса требует немного больше внимания, чем консервирование типов простых объектов. Основная причина заключается в том, что Python консервирует данные экземпляра (обычно атрибут __dict__) и имя класса, но не код для класса. Когда Python восстанавливает экземпляр класса, он пытается импортировать модуль, содержащий описание класса, используя точные имена класса и модуля (включая любые префиксы пути к пакету) такими, какими они были во время консервирования экземпляра. Также заметьте, что описания класса должны располагаться на верхнем уровне модуля, что означает, что они не могут быть вложенными классами (классами, объявленными внутри других классов или функций).
Когда экземпляры класса восстанавливаются, обычно их метод __init__() не вызывается заново. Наоборот, Python создает родовой экземпляр класса, использует атрибуты экземпляра, которые были законсервированы, и устанавливает атрибут экземпляра __class__ так, чтобы он указывал на оригинальный класс.
Классы нового стиля (new-style class), появившиеся в Python 2.2, опираются на слегка отличный механизм восстановления. Несмотря на то, что результат этого процесса по существу такой же, как и с классами старого стиля, Python использует функцию _reconstructor() модуля copy_reg, чтобы восстановить экземпляры классов нового стиля.
Если вы хотите изменить поведение консервирования по умолчанию для экземпляров класса нового или старого стиля, вы можете описать специальные методы класса, а именно: __getstate__() и __setstate__ () - которые будут вызываться Python во время сохранения и восстановления информации о состоянии для экземпляров этого класса. В следующих разделах будут приведены некоторые примеры использования этих специальных методов.
Пока давайте рассмотрим экземпляр простого класса. Для начала мы создали
модуль Python persist.py, в котором содержится следующее описание
класса нового стиля:
Листинг 11. Описание класса нового стиля
class Foo(object):
def __init__(self, value):
self.value = value
Теперь мы можем законсервировать экземпляр Foo и изучить его
представление:
Листинг 12. Консервирование экземпляра 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, хранятся в консервированном объекте. Если бы мы законсервировали этот экземпляр в файл и восстановили бы его позже (или на другой машине), Python попытался бы импортировать модуль Orbtech.examples.persist, и если бы не смог это сделать, возбудил бы исключение. Подобные ошибки случились бы, если бы мы переименовали класс, модуль или переместили модуль в другой каталог.
Ниже приведена ошибка, которую выдаст Python, если мы переименуем класс
Foo, а затем попытаемся загрузить ранее законсервированный экземпляр
Foo:
Листинг 13. Попытка загрузить законсервированный экземпляр
переименованного класса Foo
>>> 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
>>> 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
Ниже, в разделе Эволюция схемы, мы увидим, как управлять изменениями такого вида, не разрушая существующие консервированные объекты.
Специальные методы состояния
Ранее мы упоминали о том, что несколько типов объектов, как, например,
файловые объекты, не могут быть законсервированы. Один из способов управлять
атрибутами экземпляра, которые не являются консервируемыми объектами, - это
воспользоваться специальными методами, доступными для модифицирования состояния
экземпляра класса: __getstate__() и __setstate__(). Ниже
приведен пример нашего класса , который мы модифицировали, чтобы управлять
атрибутом файлового объекта:
Листинг 15. Обработка атрибутов неконсервируемого
экземпляра
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
Когда экземпляр Foo будет законсервирован, Python законсервирует только те значения, которые возвращены в него при вызове метода __getstate__() этого экземпляра. Подобным образом во время восстановления Python передаст в метод __setstate__() этого экземпляра восстановленные значения в качестве аргумента. Внутри метода _setstate_() мы можем воссоздать файловый объект, опираясь на имя и информацию о положении, которые мы законсервировали, и присвоить файловый объект атрибуту logfile этого экземпляра.
Эволюция схемы
Изменение имени класса
def __setstate__(self, state):
self.__dict__.update(state)
self.__class__ = NewClassName
Добавление и удаление атрибутов
class Person(object):
def __init__(self, firstname, lastname):
self.firstname = firstname
self.lastname = lastname
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)
Модификации модуля
Заключение
Персистентность объектов зависит от возможностей сериализации используемого языка программирования. Для объектов Python под этим подразумевается консервирование (pickling). Питоновские законсервированные объекты являются прочным и надежным основанием для эффективного управления персистентностью объектов Python. Ниже, в разделе Ресурсы, вы найдете информацию о системах, которые построены на Питоновской возможности консервирования.
Ресурсы
- Web-сайт Python - отправная точки для всего Питоновского.
- Официальная информация о Pickle доступна на Web-сайте Python.
- ZOBD, объектная база данных Z - это часть Zope, которая может также независимо использоваться для управления объектами Python.
- Web-сайт Prevayler содержит исчерпывающие сведения о философии распространения (prevalence philosophy).
- PyPerSyst, порт Prevayler на Python, доступен на SourceForge.
- За более подробной информацией о Prevayler отсылаем вас к "Введению в распространение объектов" ("An introduction to object prevalence", developerWorks, август 2002).
- Если вы интересуетесь консервированием объектов Python как XML, прочтите "Питоновская обработка документов XML как объектов" ("On the Pythonic treatment of XML documents as objects", developerWorks, август 2000).
- Интервью об использовании Python и Perl с IBM DB2 - "Верблюд и змея или "Обмани пророка": разработка с открытым исходным кодом с Python, Perl и IBM DB2" ("The Camel and the Snake, or 'Cheat the Prophet': Open Source Development with Perl, Python, and DB2").
- Ресурсы для разработчиков Linux в зоне Linux developerWorks.
Об авторе
Патрик О'Брайен - программист Python, консультант и преподаватель. Он автор PyCrust и разработчик проекта PythonCard. Совсем недавно Патрик руководил группой PyPerSyst, которая переносила Prevayler на Python. Сейчас он продолжает вести этот проект, но для новой интересной области. За более подробной информацией о Патрике и его работе обращайтесь на Web-сайт Orbtech или пишите ему на pobrien@orbtech.com.
Автор: Патрик О'Брайен (Patrick O'Brien), программист Python, Orbtech