Библиотека 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__
для корректной сериализации/десериализации
его объектов.