Рубрики
Без рубрики

pickle – Сериализация объекта

Автор оригинала: Doug Hellmann.

Цель:

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

Модуль pickle реализует алгоритм преобразования произвольного объекта Python в серию байтов. Этот процесс также называется сериализацией объекта. Затем байтовый поток, представляющий объект, может быть передан или сохранен, а затем реконструирован для создания нового объекта с такими же характеристиками.

Предупреждение

В документации для pickle четко указано, что он не предлагает никаких гарантий безопасности. Фактически, при распаковке данных можно выполнить произвольный код. Будьте осторожны, используя pickle для межпроцессного взаимодействия или хранения данных, и не доверяйте данным, которые нельзя проверить как безопасные. См. Модуль hmac для примера безопасного способа проверки источника маринованного источника данных.

Кодирование и декодирование данных в строках

В этом первом примере dumps () используется для кодирования структуры данных в виде строки, а затем строка выводится на консоль. Он использует структуру данных, состоящую из полностью встроенных типов. Экземпляры любого класса можно мариновать, как будет показано в следующем примере.

pickle_string.py

import pickle
import pprint

data  [{'a': 'A', 'b': 2, 'c': 3.0}]
print('DATA:', end' ')
pprint.pprint(data)

data_string  pickle.dumps(data)
print('PICKLE: {!r}'.format(data_string))

По умолчанию рассол будет записан в двоичном формате, наиболее совместимом при совместном использовании между программами Python 3.

$ python3 pickle_string.py

DATA: [{'a': 'A', 'b': 2, 'c': 3.0}]
PICKLE: b'\x80\x03]q\x00}q\x01(X\x01\x00\x00\x00cq\x02G@\x08\x00
\x00\x00\x00\x00\x00X\x01\x00\x00\x00bq\x03K\x02X\x01\x00\x00\x0
0aq\x04X\x01\x00\x00\x00Aq\x05ua.'

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

pickle_unpickle.py

import pickle
import pprint

data1  [{'a': 'A', 'b': 2, 'c': 3.0}]
print('BEFORE: ', end' ')
pprint.pprint(data1)

data1_string  pickle.dumps(data1)

data2  pickle.loads(data1_string)
print('AFTER : ', end' ')
pprint.pprint(data2)

print('SAME? :', (data1 is data2))
print('EQUAL?:', (data1  data2))

Новый созданный объект равен оригиналу, но не совпадает с ним.

$ python3 pickle_unpickle.py

BEFORE:  [{'a': 'A', 'b': 2, 'c': 3.0}]
AFTER :  [{'a': 'A', 'b': 2, 'c': 3.0}]
SAME? : False
EQUAL?: True

Работа с потоками

В дополнение к dumps () и load () , pickle предоставляет удобные функции для работы с файловыми потоками. Можно записать несколько объектов в поток, а затем прочитать их из потока, не зная заранее, сколько объектов записано или насколько они велики.

pickle_stream.py

import io
import pickle
import pprint


class SimpleObject:

    def __init__(self, name):
        self.name  name
        self.name_backwards  name[::-1]
        return


data  []
data.append(SimpleObject('pickle'))
data.append(SimpleObject('preserve'))
data.append(SimpleObject('last'))

# Simulate a file.
out_s  io.BytesIO()

# Write to the stream
for o in data:
    print('WRITING : {} ({})'.format(o.name, o.name_backwards))
    pickle.dump(o, out_s)
    out_s.flush()

# Set up a read-able stream
in_s  io.BytesIO(out_s.getvalue())

# Read the data
while True:
    try:
        o  pickle.load(in_s)
    except EOFError:
        break
    else:
        print('READ    : {} ({})'.format(
            o.name, o.name_backwards))

В этом примере моделируются потоки с использованием двух буферов BytesIO . Первый получает маринованные объекты, а его значение передается второму, откуда выполняется чтение load () . В простом формате базы данных можно также использовать соленые огурцы для хранения объектов. Модуль полки – одна из таких реализаций.

$ python3 pickle_stream.py

WRITING : pickle (elkcip)
WRITING : preserve (evreserp)
WRITING : last (tsal)
READ    : pickle (elkcip)
READ    : preserve (evreserp)
READ    : last (tsal)

Помимо хранения данных, соленья удобны для межпроцессного взаимодействия. Например, os.fork () и os.pipe () можно использовать для создания рабочих процессов, которые читают инструкции задания из одного канала и записывают результаты в другой канал. Основной код для управления пулом рабочих и отправки заданий и получения ответов можно использовать повторно, поскольку объекты задания и ответа не обязательно должны основываться на конкретном классе. При использовании каналов или сокетов не забывайте промывать после сброса каждого объекта, чтобы протолкнуть данные через соединение на другой конец. См. Модуль multiprocessing для многоразового диспетчера пула рабочих.

Проблемы с реконструкцией объектов

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

pickle_dump_to_file_1.py

import pickle
import sys


class SimpleObject:

    def __init__(self, name):
        self.name  name
        l  list(name)
        l.reverse()
        self.name_backwards  ''.join(l)


if __name__  '__main__':
    data  []
    data.append(SimpleObject('pickle'))
    data.append(SimpleObject('preserve'))
    data.append(SimpleObject('last'))

    filename  sys.argv[1]

    with open(filename, 'wb') as out_s:
        for o in data:
            print('WRITING: {} ({})'.format(
                o.name, o.name_backwards))
            pickle.dump(o, out_s)

При запуске сценарий создает файл на основе имени, указанного в качестве аргумента в командной строке.

$ python3 pickle_dump_to_file_1.py test.dat

WRITING: pickle (elkcip)
WRITING: preserve (evreserp)
WRITING: last (tsal)

Упрощенная попытка загрузить получившиеся маринованные объекты не удалась.

pickle_load_from_file_1.py

import pickle
import pprint
import sys

filename  sys.argv[1]

with open(filename, 'rb') as in_s:
    while True:
        try:
            o  pickle.load(in_s)
        except EOFError:
            break
        else:
            print('READ: {} ({})'.format(
                o.name, o.name_backwards))

Эта версия не работает, потому что недоступен класс SimpleObject .

$ python3 pickle_load_from_file_1.py test.dat

Traceback (most recent call last):
  File "pickle_load_from_file_1.py", line 15, in 
    o = pickle.load(in_s)
AttributeError: Can't get attribute 'SimpleObject' on 

Исправленная версия, которая импортирует SimpleObject из исходного скрипта, успешно работает. Добавление этого оператора импорта в конец списка импорта позволяет сценарию найти класс и построить объект.

from pickle_dump_to_file_1 import SimpleObject

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

$ python3 pickle_load_from_file_2.py test.dat

READ: pickle (elkcip)
READ: preserve (evreserp)
READ: last (tsal)

Невыбираемые объекты

Не все предметы можно мариновать. Сокеты, дескрипторы файлов, подключения к базам данных и другие объекты, состояние выполнения которых зависит от операционной системы или другого процесса, может не иметь возможности сохранить значимым образом. Объекты, которые имеют атрибуты, не допускающие выбора, могут определять __getstate __ () и __setstate __ () для возврата подмножества состояния экземпляра, подлежащего маринованию.

Метод __getstate __ () должен возвращать объект, содержащий внутреннее состояние объекта. Один удобный способ представить это состояние – использовать словарь, но значение может быть любым выбираемым объектом. Состояние сохраняется и передается в __setstate __ () , когда объект загружается из рассола.

pickle_state.py

import pickle


class State:

    def __init__(self, name):
        self.name  name

    def __repr__(self):
        return 'State({!r})'.format(self.__dict__)


class MyClass:

    def __init__(self, name):
        print('MyClass.__init__({})'.format(name))
        self._set_name(name)

    def _set_name(self, name):
        self.name  name
        self.computed  name[::-1]

    def __repr__(self):
        return 'MyClass({!r})>{!r})'.format(
            self.name, self.computed)

    def __getstate__(self):
        state  State(self.name)
        print('__getstate__ -> {!r}'.format(state))
        return state

    def __setstate__(self, state):
        print('__setstate__({!r})'.format(state))
        self._set_name(state.name)


inst  MyClass('name here')
print('Before:', inst)

dumped  pickle.dumps(inst)

reloaded  pickle.loads(dumped)
print('After:', reloaded)

В этом примере используется отдельный объект State для хранения внутреннего состояния MyClass . Когда экземпляр MyClass загружается из рассола, __setstate __ () передается экземпляр State , который он использует для инициализации объекта.

$ python3 pickle_state.py

MyClass.__init__(name here)
Before: MyClass('name here'))
__getstate__ -> State({'name': 'name here'})
__setstate__(State({'name': 'name here'}))
After: MyClass('name here'))

Предупреждение

Если возвращаемое значение ложно, то __setstate __ () не вызывается, когда объект не выбран.

Циркулярные ссылки

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

digraph pickle_example {
  “а”; «корень» -> «б»; «а» -> «б»; «б» -> «а»; «б» -> «в»; «а» -> «а»; }” />

Сбор структуры данных с помощью циклов

pickle_cycle.py

import pickle


class Node:
    """A simple digraph
    """
    def __init__(self, name):
        self.name  name
        self.connections  []

    def add_edge(self, node):
        "Create an edge between this node and the other."
        self.connections.append(node)

    def __iter__(self):
        return iter(self.connections)


def preorder_traversal(root, seenNone, parentNone):
    """Generator function to yield the edges in a graph.
    """
    if seen is None:
        seen  set()
    yield (parent, root)
    if root in seen:
        return
    seen.add(root)
    for node in root:
        recurse  preorder_traversal(node, seen, root)
        for parent, subnode in recurse:
            yield (parent, subnode)


def show_edges(root):
    "Print all the edges in the graph."
    for parent, child in preorder_traversal(root):
        if not parent:
            continue
        print('{:>5} -> {:>2} ({})'.format(
            parent.name, child.name, id(child)))


# Set up the nodes.
root  Node('root')
a  Node('a')
b  Node('b')
c  Node('c')

# Add edges between them.
root.add_edge(a)
root.add_edge(b)
a.add_edge(b)
b.add_edge(a)
b.add_edge(c)
a.add_edge(a)

print('ORIGINAL GRAPH:')
show_edges(root)

# Pickle and unpickle the graph to create
# a new set of nodes.
dumped  pickle.dumps(root)
reloaded  pickle.loads(dumped)

print('\nRELOADED GRAPH:')
show_edges(reloaded)

Перезагруженные узлы не являются одним и тем же объектом, но связь между узлами сохраняется, и перезагружается только одна копия объекта с несколькими ссылками. Оба этих оператора можно проверить, исследуя значения id () для узлов до и после прохождения через pickle.

$ python3 pickle_cycle.py

ORIGINAL GRAPH:
 root ->  a (4315798272)
    a ->  b (4315798384)
    b ->  a (4315798272)
    b ->  c (4315799112)
    a ->  a (4315798272)
 root ->  b (4315798384)

RELOADED GRAPH:
 root ->  a (4315904096)
    a ->  b (4315904152)
    b ->  a (4315904096)
    b ->  c (4315904208)
    a ->  a (4315904096)
 root ->  b (4315904152)

Смотрите также