| Простая сериализация |
|
Существует несколько способов сохранения данных на диск для последующего использования. Процесс сохранения данных на диск без сохранения отношений между частями данных мы называем «простой се-риализацией». Различия между простой и реляционной сериализацией мы обсудим в разделе, описывающем реляционную сериализацию. Pickle Первый и, пожалуй, самый основной механизм «простой сериализа-ции» в языке Python представлен модулем pickle, входящим в состав стандартной библиотеки языка. Если подумать о консервировании в кулинарном смысле, идея обеспечения сохранности продуктов питания состоит в том, чтобы законсервировать их в банке для последующего использования. Кулинарная концепция прекрасно укладывается в образ действия модуля pickle. С помощью этого модуля вы можете записать объект на диск, завершить работу программы, вернуться позднее, снова запустить программу, прочитать объект с диска и продолжить взаимодействовать с ним. Какими возможностями обладает модуль pickle? Ниже приводится список, взятый из описания модуля pickle в документации к стандартной библиотеке языка Python, где перечислены типы объектов, которые могут сохраняться с его помощью: • None, True и False •
Целые числа, длинные целые, числа с плавающей точкой, ком • Обычные строки и строки Юникода •
Кортежи, списки, множества и словари, содержащие только
те объ • Функции, определенные на верхнем уровне в модуле • Встроенные функции, определенные на верхнем уровне в модуле • Классы, определенные на верхнем уровне в модуле • Экземпляры классов, у которых атрибуты _ _diet_ _ и _ _setstate_ _() могут сохраняться с помощью модуля pickle Ниже показано, как выполняется сериализация объекта на диск с помощью модуля pickle:
А вот как выглядит файл с сохраненными в нем данными: Вы можете попытаться изучить формат файлов, создаваемых модулем pickle, и создавать их вручную, но мы не рекомендуем делать это. Ниже демонстрируется, как восстановить сохраненные ранее данные:
Обратите внимание, что для восстановления данных мы использовали объект, имя которого отличается от имени объекта, который сохранялся в файле. Не забывайте, что имя - это всего лишь способ сослаться на объект. Интересно отметить, что совершенно необязательно, чтобы между файлами и сохраняемыми объектами существовало отношение «один к одному». Вы можете сохранять в одном и том же файле столько объектов, сколько места хватит на жестком диске или в файловой системе. Ниже приводится пример сохранения нескольких словарей в одном файле:
Мы создали список словарей, объект файла, открытого в режиме для записи, затем вbinолнили обход списка словарей и сериализовали каждый из них в один и тот же файл. Обратите внимание, это тот же самый метод сохранения, который использовался выше для сохранения одного объекта в файл, только там мы не вbinолняли итерации и не вызывали метод dump() несколько раз. Ниже приводится пример восстановления объектов из файла, содержащего несколько объектов, и их вывод:
Здесь мы создали объект файла, созданного в предыдущем примере, открытого в режиме для чтения, и повторяли попытки загружать объекты из файла, пока не было возбуждено исключение EOFError. Как видите, словари, полученные из файла, оказались теми же самыми (и следуют в том же порядке), что и словари, которые мы записали в файл. Но мало того, что мы можем сохранять объекты простых встроенных типов, мы можем также сохранять объекты созданных нами типов. Ниже приводится содержимое модуля, который мы будем использовать в двух следующих примерах. Этот модуль содержит определение нашего собственного класса, экземпляры которого мы попробуем сохранить, а потом восстановить:
Следующий модуль импортирует модуль с нашим классом и сохраняет экземпляр этого класса в файл с помощью модуля pickle:
В этом примере мы импортировали модуль с нашим классом, создали экземпляр этого класса, добавили в объект несколько элементов, затем сериализовали его. В процессе своей работы этот модуль ничего не выводит. Далее приводится модуль, который импортирует модуль с нашим классом и затем загружает экземпляр этого класса из файла:
Ниже приводится вывод, полученный в ходе восстановления данных из файла:
Для программного кода, вbinолняющего восстановление данных, совершенно необязательно явно импортировать наш класс. Однако код должен иметь возможность отыскать модуль, в котором определяется наш класс. Ниже приводится модуль, который не импортирует модуль с определением класса:
Ниже приводится вывод, полученный в результате запуска модуля, который не импортирует класс:
А вот что было получено от того же самого модуля, после того как он и файл с данными были скопированы в другой каталог, где он и был запущен:
Последняя строка сообщает о неудачной попытке выполнить импорт, потому что модуль pickle не смог загрузить наш модуль с определением класса. Модуль pickle будет пытаться отыскать модуль, содержащий ваш класс, и импортировать его, чтобы иметь возможность вернуть объект того же типа, что и сохраненный в файле. Все предыдущие примеры использования модуля pickle прекрасно работают, но существует еще один момент, о котором мы еще не упоминали. По умолчанию модуль pickle использует протокол сохранения pickle. dump(Object_to_pickle, pickle_f ile). Протокол - это спецификация формата записи в файл. Протокол по умолчанию использует формат, практически доступный человеку для восприятия, как было показано выше. Другая разновидность протокола - это двоичный формат. Вы можете предпочесть использовать двоичный формат, если заметите, что операция сохранения ваших объектов начинает занимать существенное время. Ниже приводится сравнение использования протокола по умолчанию и двоичного протокола:
Первый файл с данными, созданный нами (с именем default . pkl ), будет содержать данные в формате по умолчанию, практически доступном человеку для восприятия. Второй файл (с именем binary . pkl ) будет содержать данные в двоичном формате. Обратите внимание, что мы открыли файл default . pkl в обычном режиме для записи ('w'), а файл binary . pkl - в режиме записи двоичных данных ('wb'). Единственное различие между двумя вызовами метода dump() заключается в том, что при сохранении в двоичном формате методу передается один дополнительный аргумент: число -1, означающее, что будет использоваться «высший» протокол, которым в настоящее время является двоичный протокол.
А так выглядит шестнадцатеричный дамп файла с данными, сохраненными при использовании протокола по умолчанию:
Ниже приводится шестнадцатеричный дамп двоичного файла с данными: В этом просмотре дампа нет никакой необходимости, потому что мы можем воспользоваться простой утилитой cat, чтобы прочитать содержимое файла с данными, сохраненными при использовании протокола по умолчанию:
cPickle В стандартной библиотеке языка Python присутствует еще одна реализация библиотеки Pickle, которая стоит того, чтобы вы обратили на нее внимание. Она называется cPickle. Как явствует из имени, библиотека cPickle реализована на языке С. Ранее мы уже предлагали применять двоичный формат в случаях, когда вы начнете замечать, что на сохранение объектов требуется существенное время. В этом же случае можно попробовать использовать модуль cPickle. Интерфейс модуля cPickle в точности соответствует интерфейсу «обычного» модуля pickle. Еще одну возможность сохранения данных предоставляет модуль shelve. Модуль shelve имеет простой и удобный интерфейс, упрощающий возможность сохранения множества объектов. Под этим подразумевается возможность сохранения множества объектов в одном и том же объекте-хранилище и простого их восстановления из хранилища. Сохранение объектов в хранилище shelve напоминает использование словаря в языке Python. Ниже приводится пример, в котором открывается хранилище, в него записываются данные, затем хранилище повторно открывается и из него извлекаются сохраненные данные:
Единственное отличие между использованием shelve и простого словаря состоит в том, что объект shelve создается с помощью метода shelve. open(), а не путем создания экземпляра класса diet или с помощью фигурных скобок ({}). Еще одно отличие состоит в том, что при использовании shelve по завершении работы с данными необходимо вызывать метод close() объекта shelve. У объекта shelve имеется пара особенностей. О первой из них мы уже упоминали: по завершении работы с данными необходимо вызывать метод close(). Если этого не сделать, то любые изменения, которые были сделаны в объекте shelve, не будут сохранены. Ниже приводится пример потери данных из-за того, что объект shelve не закрывается. Для начала нам нужно создать объект shelve, сохранить в нем данные и выйти из оболочки IPython:
Теперь снова запустим IPython, откроем тот же файл хранилища, создадим в нем еще один элемент и выйдем, не закрыв объект shelve:
Теперь снова запустим оболочку IPython, откроем все тот же файл хранилища и посмотрим, что в нем имеется:
Итак, необходимо вызывать метод close() для всех объектов shelve, содержимое которых вы меняете, и которые вам хотелось бы сохранить. Другая особенность касается изменяемых объектов. Запомните, что изменяемыми объектами называются такие объекты, значение которых можно изменять без повторного присваивания этого значения переменной. Ниже мы создаем объект shelve, добавляем в него элемент, который представляет собой изменяемый объект (в данном случае -список), модифицируем изменяемый объект, а затем закрываем объект shelve:
Поскольку в этом случае вызывается метод close() объекта shelve, можно было бы ожидать, что значением ключа 'key' будет список [1]. Но это не так. Ниже приводится результат попытки открыть файл хранилища, созданного выше, и прочитать из него данные:
При попытке восстановить сохраненный ранее объект мы получили следующее:
В таком поведении нет ничего странного или неожиданного. В действительности эта особенность shelve описана в документации. Проблема состоит в том, что модификация сохраняемых изменяемых объектов не воспринимаются по умолчанию. Однако существует пара способов, позволяющих обойти этот недостаток. Первый из них специализированный и узконаправленный, второй - широкий и всеобъемлющий. Первый, специализированный, подход заключается в том, чтобы просто выполнить повторное присваивание по ключу в объекте shelve, как показано ниже: Список, к которому был добавлен элемент, сохранился. Второй, широкий и всеобъемлющий подход заключается в изменении флага writeback объекта shelve. До сих пор мы демонстрировали вызов метода shelve, open() с единственным параметром - именем файла хранилища. Но этот метод может принимать еще и другие параметры, одним из которых является флаг writeback. Если во флаге writeback передано значение True, все записи в объекте shelve, к которым вbinолнялось обращение, кэшируются в памяти и затем сохраняются при вызове метода close(). Этот прием может оказаться удобным при работе с изменяемыми объектами, но за это приходится платить. Поскольку все объекты, к которым производилось обращение, кэшируются и затем сохраняются при закрытии объекта (независимо от того, изменялись они или нет), объем используемой памяти и время на запись в файл будут расти пропорционально числу объектов в хранилище, к которым производился доступ. Поэтому, если у вас имеется большое число объектов в хранилище, к которым приходится обращаться, то лучше не устанавливать флаг writeback в значение True.
А теперь проверим, сохранились ли наши изменения:
В следующем примере мы устанавливаем во флаге writeback значение True и модифицируем содержимое списка, не вbinолняя повторное его присваивание ключу в объекте shelve: Как мы и надеялись, изменения были сохранены. Модуль shelve предлагает простой способ сохранения данных. В нем имеется пара недостатков, но в целом это очень полезный модуль. YAML В зависимости от того, кому задается вопрос, вы можете услышать разные толкования аббревиатуры YAML, например: «YAML ain't markup language» (YAML - это не язык разметки) или «yet another markup language» (еще один язык разметки). В любом случае - это формат данных, который часто используется для сохранения, восстановления и обновления данных в виде простого текста. Эти данные часто имеют иерархическую структуру. Самый простой, пожалуй, способ приступить к работе с YAML в языке Python состоит в том, чтобы установить с помощью утилиты easy_install пакет PyYAML. Но зачем нам использовать YAML, который еще требуется устанавливать, когда у нас имеется встроенный модуль pickle? Существуют две основные причины, по которым YAML оказывается предпочтительнее, чем pickle. Эти две причины не делают применение YAML наилучшим во всех ситуациях, но при определенных обстоятельствах они приобретают особую значимость. Во-первых, формат YAML пригоден для восприятия человеком. Его синтаксис напоминает синтаксис конфигурационных файлов. Если у вас возникают ситуации, когда необходимо предоставить возможность редактирования конфигурационных файлов, YAML будет отличным выбором. Во-вторых, синтаксические анализаторы языка YAML реализованы во многих других языках. Если вам требуется обеспечить обмен данными между приложением на языке Python и приложением, написанном на другом языке программирования, YAML может стать неплохим решением проблемы. После установки PyYAML вы получаете возможность сохранять и восстанавливать данные в формате YAML. Ниже приводится пример сохранения простого словаря:
Этот пример достаточно прост, чтобы вы могли разобраться в нем самостоятельно, и, тем не менее, мы рассмотрим его. Первое, что здесь делается, - выполняется импортирование модуля YAML (с именем yaml). Затем открывается файл в режиме для записи, который будет использоваться для сохранения данных. Далее создается словарь (с именем d), содержащий данные, которые требуется сохранить. После этого мы сохраняем словарь (с именем d) с помощью функции dump() из модуля yaml. В качестве параметров функции dump() передаются: словарь, который требуется сохранить, выходной файл и параметр, сообщающий библиотеке YAML, что запись должна производиться в блочном стиле, а не в стиле, заданном по умолчанию, который отчасти напоминает преобразование сохраняемого объекта данных в строку. Ниже показано, как выглядит содержимое файла с данными в формате YAML:
Когда необходимо восстановить данные, мы вbinолняем операции, обратные тем, что вbinолнялись в примере с применением функции dump(). Ниже показано, как получить данные из файла YAML:
Как и в примере с функцией dump(), мы сначала импортируем модуль поддержки языка YAML (yaml). Затем создаем объект файла. На этот раз мы открываем файл на диске в режиме для чтения. Наконец вызывается функция load() из модуля yaml. Функция load() возвращает словарь, эквивалентный исходному словарю. Вы наверняка поймаете себя на том, что при использовании модуля yaml вы реализуете цикл создания данных, сохранения их на диске, затем восстановления с диска и так далее.
Вот как выглядит содержимое файла с данными в формате YAML:
Возможно, вам не обязательно сохранять свои данные в формате, доступном для восприятия человеком, поэтому попробуем сохранить словарь из предыдущего примера не в блочном режиме. Ниже показано, как сохранить тот же самый словарь не в блочном режиме: Очень похоже на содержимое файла, записанного в блочном режиме, за исключением списка значений переменной ьam. Различия между этими режимами начинают проявляться с появлением дополнительных уровней вложенности и структур данных, напоминающих массивы, таких как списки и словари. Рассмотрим пару примеров, чтобы увидеть различия. Но прежде заметим, что исследовать примеры будет проще, если отказаться от просмотра YAML-файлов с помощью утилиты cat. Аргумент с файлом в функции dump() из модуля yaml является необязательным. (Фактически в документации к PyYAML объект типа «file» называется «stream» (поток), но в действительности большой роли это не играет.) Если функция dump() не получит аргумент с файлом (или «потоком»), она выведет сериализованный объект в поток стандартного вывода. Поэтому в следующем примере мы опустили аргумент с объектом типа file и выводим результат работы функции. Ниже сравниваются некоторые структуры данных, которые сериали-зуются в блочном и в не блочном режимах. В примерах, где присутствует аргумент def ault_f low_style, используется блочный режим форматирования, а в примерах, где аргумент default_flow_style отсутствует, используется не блочный режим форматирования:
А если нам потребуется сериализовать наш собственный класс? В этом случае модуль yaml ведет себя практически точно так же, как и модуль pickle. В следующем примере используется тот же самый модуль custom_class, который использовался в примере с модулем pickle. Ниже приводится содержимое модуля, который импортирует модуль custom_class, создает экземпляр класса MyClass, добавляет несколько элементов в объект и затем сериализует его:
Когда мы запустили этот модуль, получили следующий вывод: То есть ничего. Это означает, что все идет так, как надо. Ниже приводится модуль, обратный предыдущему: Этот сценарий импортирует модули yaml и custom_class, создает объект файла для чтения данных из файла, созданного предыдущим сценарием, загружает объект из файла и выводит его. Когда мы запустили этот сценарий, то получили следующее:
Точно такой же результат мы получили в примере, использующем модуль pickle, демонстрировавшемся ранее, откуда следует, что модуль yaml проявляет именно такое поведение, какое мы и предполагали увидеть. ZODB Еще один способ сериализации данных основан на применении модуля ZODB. ZODB означает «Zope Object Database» (объектная база данных Zope). В простейших случаях использование ZODB напоминает сериализацию с помощью модуля pickle или yaml, но ZODB обладает возможностью расти вместе с вашими потребностями. Например, ZODB предоставляет механизм транзакций - на случай, если вам потребуется обеспечить атомарность своих операций. А если вам потребуется легко масштабируемое решение, вы можете использовать ZEO, систему распределенного хранения объектов. База данных ZODB имела все шансы попасть не в раздел, описывающий «простую сериализацию», а в раздел, где рассказывается о «реляционной сериализации». Однако эта объектная база данных не совсем точно соответствует тому, что мы привыкли называть реляционными базами данных, хотя вы без труда можете устанавливать отношения между объектами. Кроме того, мы продемонстрируем лишь некоторые из наиболее основных возможностей ZODB, поэтому в наших примерах она больше напоминает модуль shelve, чем реляционную базу данных. Поэтому мы и решили оставить ZODB в разделе, рассказывающем о «простой сериализации». Установка ZODB выполняется просто — достаточно запустить команду easy_install ZODB3. Модуль ZODB имеет ряд зависимостей, но утилита easy_install благополучно разрешит их, и загрузит и установит все, что необходимо. Для примера простейшего использования ZODB создадим объект-хранилище ZODB и добавим в него словарь и список. Ниже приводится программный код, вbinолняющий сериализацию словаря и списка:
По сравнению с pickle или YAML для инициализации работы с ZODB требуется написать на пару строк программного кода больше, но как только хранилище будет создано и инициализировано, оно используется ничуть не сложнее других альтернатив. Этот пример достаточно очевиден, особенно если учесть, что мы уже рассматривали другие примеры сохранения данных. И, тем не менее, мы быстро пройдемся по нему. Во-первых, мы импортируем несколько модулей ZODB, а именно ZODB, ZODB. FileStorage и transaction. (Мы хотели бы здесь сделать небольшое замечание. Импортирование модуля, в имени которого отсутствует идентификационный префикс, выглядит несколько странно. Создается впечатление, что импортируемый модуль transaction должен иметь префикс ZODB. Но как бы то ни было, имя модуля такое, какое есть, и вам просто достаточно знать об этом. А теперь можно двигаться дальше.) Затем создается объект FileStorage, которому указывается имя файла, который будет использоваться как база данных. Затем создается объект DB и подключается к объекту FileStorage. Затем объект базы данных открывается с помощью метода open() и обретается ссылка на корневой узел объекта. С этого момента мы можем добавлять в корень объекта свои структуры данных, что мы и делаем, используя импровизированные список и словарь. После этого мы подтверждаем изменения с помощью функции transaction, commit() и затем закрываем соединение с базой данных вызовом метода conn. close(). Как только будет создан контейнер хранилища данных (как объект файла хранилища в этом примере) и запись данных будет подтверждена, у вас может появиться потребность восстановить эти данные. В следующем примере мы открываем ту же самую базу данных, но на этот раз мы читаем данные из файла, а не записываем в него:
И если запустить этот сценарий после того, как база данных будет наполнена, мы могли бы увидеть следующее:
При описании других механизмов сохранения данных мы рассматривали примеры сериализации своих собственных классов, поэтому мы покажем, как то же самое делается с помощью ZODB. Однако на этот раз мы не будем использовать тот же самый класс MyClass (позднее объясним, почему). Как и при использовании других механизмов, мы просто объявим свой класс, создадим экземпляр этого класса и затем передадим его механизму сериализации для сохранения на диске. Ниже приводится определение класса, который мы будем использовать в этот раз:
Это очень простой класс, имитирующий банковский счет и предназначенный для управления денежными средствами. Мы также определили исключение OutOfFunds, назначение которого объясним позже. Класс Account наследует класс perslstent. Perslstent. (Что касается модуля perslstent, мы опять могли бы сделать высокопарное отступление об уместности значимого префикса в имени модуля, который предполагается использовать. Как при беглом знакомстве с этим программным кодом определить, что он использует ZODB? Никак. Но не будем ходить по кругу.) Наследование от класса perslstent. Perslstent позволяет задействовать скрытые механизмы и облегчает для ZODB сериализа-цию этих данных. В определении класса мы создали собственные реализации методов преобразования класса в строковую форму _ _str_ _ и _ _rep r_ _. Позднее вы увидите их в действии. Мы также создали методы deposit() и withdraw(). Оба метода изменяют атрибут ьalance объекта в сторону увеличения или уменьшения, в зависимости от того, какой метод вызывается. Метод withdraw() проверяет, достаточно ли денег на балансе (в атрибуте ьalance), прежде чем списать запрошенную сумму. Если денег недостаточно, метод withdraw() возбуждает исключение OutOfFunds, упоминавшееся выше. Оба метода, deposit() и withdrawn, возвращают остаток средств на счете после выполнения операции. Ниже приводится программный код, который сохраняет только что описанный класс:
Этот пример практически идентичен предыдущему примеру использования ZODB, где мы сохраняли словарь и список. Только здесь мы импортируем свой собственный модуль, создаем два экземпляра нашего класса и сохраняем эти два объекта в базе данных ZODB. Эти два объекта - счет noah и счет Jeremy, каждый из которых имеет на балансе 1000 (предположим, $1000.00, но мы не идентифицировали, в какой валюте исчисляется сумма на счете). Ниже приводится результат работы этого примера:
А если запустить модуль, отображающий содержимое базы данных ZODB, вот, что мы получим:
Наш пример не только создал два объекта, как ожидалось, но и сохранил их на диск для последующего использования. А как нам открыть базу данных и изменить суммы на счетах? Все наши усилия были бы бессмысленны, не будь такой возможности. Ниже приводится фрагмент, открывающий базу данных, созданную ранее, и вbinолняющий перевод 300 (по-видимому, долларов) со счета noah на счет jeremy:
Ниже приводятся результаты работы этого сценария: А если запустить наш сценарий, отображающий содержимое базы данных ZODB, то увидим, что данные сохранились:
Сумма на счете noah уменьшилась с 1000 до 700, а сумма на счете Jeremy увеличилась с 1000 до 1300. Причина, по которой мы отказались от использования класса MyClass, состоит в том, что нам хотелось продемонстрировать работу с транзакциями. Один из классических способов сделать это - продемонстрировать их использование при работе с банковскими счетами. Если вам требуется гарантировать благополучный перевод средств с одного счета на другой без потери средств, то транзакции будут первым инструментом, на который стоит обратить внимание. Ниже приводится пример, где используются транзакции в цикле и показано, что деньги никуда не пропадают:
Это некоторая модификация предыдущего примера сценария, вbinолняющего перевод средств. Только на этот раз вместо одного перевода он выполняет переводы по 300 единиц со счета noah на счет Jeremy, пока на счету noah не окажется недостаточно средств для перевода. В момент, когда на счету оказывается недостаточно средств, сценарий выводит сообщение о том, что возникло исключение, и информацию о текущем состоянии счетов. После этого вызывается метод abort() транзакции и ввыполнение цикла прерывается. Кроме того, сценарий выводит информацию до и после цикла транзакций. Пока транзакции совершаются, и до и после операции общий объем средств на счетах составляет 2000, поскольку изначально на каждом счете имелась сумма 1000. Ниже приводится результат запуска этого сценария:
Перед началом цикла переводов на счете noah имелась сумма 700 единиц и на счете Jeremy- 1300 единиц, итого 2000. Когда возникло исключение OutOfFunds, на счете noah имелось 100 единиц и на счете Jeremy -2200, итого 2300. В блоке «AFTER TRANSFER» (после перевода) на счете noah осталось 100 единиц и на счете Jeremy — 1900, итого 2000. Итак, когда возникло исключение, перед тем как был вызван метод transaction. abort(), имелись лишние 300 единиц, появление которых невозможно было бы объяснить. Но прерывание транзакции ликвидировало эту проблему. База данных ZODB представляет собой решение, занимающее промежуточное положение между простыми и реляционными инструментами. Она проста в использовании. Объект, сохраняемый на диске, соответствует объекту в памяти как до сохранения, так и после восстановления. Но у этого инструмента имеются такие дополнительные особенности, как транзакции. База данных ZODB стоит того, чтобы на нее обратили внимание, когда изначально требуется достаточно простой механизм отображения объектов, расширенные возможности которого могут потребоваться позже. В заключение раздела о простой сериализации: иногда все, что вам требуется, - это просто сохранять и восстанавливать объекты Python. Все инструменты, которые мы рассмотрели здесь, прекрасно справляются с этой задачей. У каждого из них есть свои сильные и слабые стороны. Когда возникнет такая необходимость, вы сможете заняться исследованием и выяснить, какой из инструментов лучше подходит для вас и вашего проекта.
Related Articles
Set as favorite
Bookmark
Email This
Hits: 483 Комментарии (0)RSS feed CommentsНаписать комментарий |