Библиотека pickle

Сериализация и десериализация

Под сериализацией понимается процесс перевода какой-либо структуры данных в последовательность битов. Десериализация является обратной опреацией и восстанавливает структуры данных из последовательности битов.

Сериализация имеет ряд применений. Один из них - передача объектов по сети или по другим каналам обмена данными. Например, у вас есть распеделенное приложение, разные части котрого обмениваются данными со сложной структурой. Для типов данных, объекты которых надо передавать, пишется код, выполняющий сериализацию и десериализацию. Перед отправкой объекта вызывается код сериализации, в результате получается документ, например, в формате XML или JSON. Полученный документ отправляется приложению-получателю. Получатель вызывает код десериализации и восстанавливает объект того же типа с теми же данными, что были до сериализации.

Другой способ применения - дамп объектов в файл с целью сохранения их состояния. Такой подход полезен в целях консервирования важных объектов для их последующего восстановления после перезапуска приложения. Например, вы решаете задачу моделирования физической системы. Ваше приложение позволяет вам взаимодействовать с сиетмой в режиме реального времени. Прошло какое-то количество времени, модель уже отличается от ее изначального состояния. Пусть текущее состояние модели является для вас каким-то важным, и вам хотелось бы его сохранить. Используя сериализацию, вы просто записываете состояние вашей модели в файл. На случай падения вашей программы или неправильного воздействия на вашу систему, вы можете восстановить сохраненое состояние и продолжить работу.

Кстати, знакомая многим возможность сохранения/загрузки в игре - ни что иное, как пример использования сериализации/десериализации.

Текстовая сериализация

С лабы №3 вы уже познакомились с возможностью сериализации объектов в текстовый формат. Один из форматов - CSV, предназначенный для записи табличных данных в файл. Также есть форматы XML, YAML, JSON, позволяющие сохранять произвольные структуры. Основной особенностью сериализации в текст является человекочитаемость.

Библиотека pickle

Оффициальная документация

pickle - одна из стандартных библиотек Python, позволяет выполнять сериализацию и десериализацию, работает с потоком байт. Формат данных, используемый библиотекой, заточен по Python, и может быть несовместим с непитоновскими программами. Однако формат данных является довольно компактным, особенно в сравнении с тектовыми форматами, и может быть дополнительно сжат при необходимости.

pickle поддерживает ряд протоколов сериализации. На момент выхода Python 3.8 их стало 6. Чем выше версия протокола, тем выше требования к версии Python. В данной работе будет использован протокол версии 3 (является протоколом по-умолчанию для Python 3.0-3.7 и является несовместимым с Python 2.x). Протокол v3 работает с объектами типа bytes.

bytes

Синтаксис bytes аналогичен синтаксису str, за исключением префикса b. Например, b'hello'. bytes является неизменяемой последовательностью интов, где каждый элемент удовлетворяет неравенству 0 <= x < 256. В то время, как значения из таблицы ASCII имеют обычную запись, остальные значения используют запись в шестнадцатеричной системе счисления с предшествующими символами \x. Например, значение 128 будет записано, как b'\x80'.

Поддерживаемые типы

pickle поддерживает следующие типы:

  • None, True и False
  • числовые типы
  • строковые и байтовые типы
  • кортежи, списки, множества и словари, состоящие только из сериализуемых объектов
  • функции (в том числе встроенные) верхнего уровня, объявленные с def (верхнего уровня = с нулевым отступом)
  • классы верхнего уровня
  • объекты классов, для которых __dict__ или результат __getstate()__ является сериализуемым.

Обратите внимание, что сериализация функций выполняется по имени, а не по значению. Т.е. библиотека сохраняет только имя функции с именем модуля, где она определена. Тоже самое справедливо и для классов.

Сериализация встроенных типов

Библиотека предоставляет следующий набор функции:

  • dump(obj, file, protocol=None, *, fix_imports=True)
  • dumps(obj, protocol=None, *, fix_imports=True)
  • load(file, *, fix_imports=True, encoding="ASCII", errors="strict")
  • loads(bytes_object, *, fix_imports=True, encoding="ASCII", errors="strict")

dump и load работают с файлом, в то время как dumps и loads работают с bytes. Если аргумент protocol не задан, то используется версия протокола по-умолчанию. fix_imports, encoding и errors нужны для совместимости с Python 2 и нами использоваться не будут.

Рассмотрим простой пример.

import pickle

# Коллекция сериализуемых объектов
data = {
    'a': [1, 2.0, 3, 4+6j, float("nan")],
    'b': ("character string", b"byte string"),
    'c': {None, True, False}
}

# Сериализация словаря data с использованием
# версии протокола по умолчанию.
print(pickle.dumps(data))

with open('data.pickle', 'wb') as f:
    # Сериализация словаря data с использованием
    # последней доступной версии протокола.
    pickle.dump(data, f, pickle.HIGHEST_PROTOCOL)

Теперь в отдельной программе выполним десериализацию.

import pickle

with open('data.pickle', 'rb') as f:
    # Версия протокола определяется автоматически,
    # нет необходимости явно указывать его.
    data = pickle.load(f)
print(data)

Обратите внимание, что файлы на чтение и запись надо открывать в двоичном режиме.

Упражнение №1

Запустите представленный выше код и убедитесь, что все объекты были десериализованы правильно. Попробуйте сериализовать другие объекты:

  • I/O объекты (например, открытый файл - результат open());
  • итераторы;
  • встроенные функции (например, print или abs);
  • функции и классы (сами классы, а не их объекты!) из подключенных библиотек (например, deque из collections);
  • самописные функции и классы.

Что из этого можно сериализовать? Можно ли с этими объектами после их десериализации взаимодействовать так, как это бы делалось до сериализации. Помните, что в скрипте с десериализацией не надо ничего импортировать, кроме pickle.

Сериализация объектов классов

В большинстве случаев написанные вами классы не потребуют дополнительного кода. Сериализация объекта класса по умолчанию состоит из сериализации класса, к которому относится объект, и сериализации __dict__. __dict__ представляет собой словарь, хранящий все атрибуты объекта и их значения. Операция сохранения объекта класса можно представить следующим образом:

def save(obj):
    return (obj.__class__, obj.__dict__)

Десериализация объекта происходит в три этапа:

  • десериализация класса и словаря атрибутов объекта
  • создание неинициализированного объекта класса
  • добавление в него атрибутов путем перезаписи его __dict__
def load(cls, attributes):
    obj = cls.__new__(cls)  # Создание объекта класса cls без вызова __init__
    obj.__dict__.update(attributes)  # Добавление в объект десериализованных атрибутов
    return obj

Обратите внимание, что pickle при десериализации будет пытаться импортировать модуль с классом самостоятельно. Если модуль не удастся импортировать, pickle попытается найти опеределение класса в запускаемом скрипте. Если и это не удастся сделать, то десериализация не будет выполнена (программа упадет с ошибкой). Эту проблему легко воспроизвести следующим образом. Создайте две разные директории. В первой директории создайте скрипт (например serializer.py) с кодом ниже и запустите его.

import pickle


class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y


if __name__ == "__main__":
    p = Point(4, 5.6)
    with open("data.pickle", "wb") as f:
        pickle.dump(p, f)

Во второй директории напишите скрипт (например deserializer.py), который десериализует объект вашего класса.

import pickle

with open("data.pickle", "rb") as f:
    p = pickle.load(f)

Вы увидите похожую ошибку:

AttributeError: Can't get attribute 'Point' on <module '__main__' from 'srv/deserializer.py'>.

Программа падает на этапе десериализации самого класса, для которого нужно будет создать объект. Тем самым, скрипт, в котором выполянется десериализация, должен иметь доступ к модулю с классом, объекты которого десериализуются. Достаточно просто вручную проимпортировать сам модуль или его класс. Это же справедливо и для самописных функций.

Упражнение №2

Попробуйте написать программу с функций резервного копирования ее состояния. Пусть в вашей программе строится двоичное дерево поиска. Ваша программа принимает от пользователя команды:

  • add X - добавить элемент в дерево;
  • find X - найти элемент в дереве;
  • delete X - удалить элемент из дерева;
  • print - распечатать все элементы дерева в отсортированном порядке;
  • clear - очистить дерево;
  • dump - создать резервную копию дерева;
  • exit - завершить работу.

Для файла с резервной копией дерева выберите одно конкретное название. Ваша программа при запуске должна пытаться восстановить состояние дерева из резервной копии, если она есть. Если это не возможно, то вы начинаете работу с пустым деревом. Напишите такую программу и убедитесь, что все работает корректно в разных случаях:

  • резервная копия отсутствует;
  • резервная копия присутствует;
  • файл резервной копии поврежден или некорректен.

Сериализация объектов с состоянием

Пусть у нас есть класс, объекты которого поддерживают внутри себя какое либо состояние (stateful). Например, поддерживают открытое соединение с базой данных, открытые файлы и т.д. Сериализация таких атрибутов не поддерживается и без написания дополонительного кода stateful объекты не возможно сериализовать. При помощи методов __setstate__ и __getstate__ можно модифицировать поведение stateful объектов при сериализации/десериализации.

class TextReader:
    """Print and number lines in a text file."""

    def __init__(self, filename):
        self.filename = filename
        self.file = open(filename)
        self.lineno = 0

    def readline(self):
        self.lineno += 1
        line = self.file.readline()
        if not line:
            return None
        if line.endswith('\n'):
            line = line[:-1]
        return "%i: %s" % (self.lineno, line)

    def __getstate__(self):
        # Копируем состояние объекта из self.__dict__, который
        # содержит все атрибуты. Всегда используйте dict.copy()
        # во избежании модификации состояния самого объекта.
        state = self.__dict__.copy()
        # Удаляем несериализуемые атрибуты.
        del state['file']
        return state

    def __setstate__(self, state):
        # Восстанавливаем атрибуты объекта.
        self.__dict__.update(state)
        # Восстанавливаем состояние открытого ранее файла. Для этого нам надо
        # заного открыть его и прочитать необходимое количество строк.
        file = open(self.filename)
        for _ in range(self.lineno):
            file.readline()
        # Создаем атрибут для file.
        self.file = file

Упражнение №3

Вспомните упражнение №3 из девятой лабы. Вашей задачей является добавить поддержку сериализации и корректной десериализации класса TextLoader и его итератора. Однако учтите, что с момента создания объекта класса содержимое директории могло поменятся, и список файлов, хранимый в объекте, может быть не актуальным. Тем самым при десериализации необходимо заного выполнять чтение списка файлов в директории. Добавьте в класс методы __getstate__ и __setstate__ для корректной сериализации/десериализации его объектов.