pickle — Serialización de objetos

Propósito:Serialización de objetos.

El módulo pickle implementa un algoritmo para convertir un objeto arbitrario Python en una serie de bytes. Este proceso es también llamado serialización de objetos. El flujo de bytes que representa al objeto puede ser transmitido o almacenado, y luego reconstruido para crear un nuevo objeto con las mismas características.

Advertencia

La documentación para pickle deja claro que no ofrece garantías de seguridad. De hecho, la deserialización de puede ejecutar código arbitrario. Ten cuidado al usar pickle para la comunicación entre procesos o el almacenamiento de datos, y no confíes en datos que no pueden ser verificados como seguros. Ve el modulo hmac para un ejemplo de una manera segura para verificar la fuente de datos serializados.

Codificación y decodificación de datos en cadenas

Este primer ejemplo utiliza dumps() para codificar una estructura de datos como una cadena, luego imprime la cadena en la consola. Utiliza una estructura de datos compuesta enteramente de tipos incorporados. Instancias de cualquier clase puede ser serializados, como se ilustrará en un ejemplo más adelante.

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))

Por defecto, la serialización se escribirá en un formato binario más compatible cuando se comparte entre programas de 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.'

Una vez que los datos se serializan, se pueden escribir en un archivo, socket, pipe, etc. Más tarde, el archivo se puede leer y los datos se pueden deserializar para construir un nuevo objeto con los mismos valores.

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))

El objeto recién construido es igual a, pero no el mismo, que el original.

$ python3 pickle_unpickle.py

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

Trabajar con flujos

Además de dumps() y loads(), pickle proporciona funciones de conveniencia para trabajar con flujos tipo archivo. Es posible escribir varios objetos en una flujo y luego leerlos de el flujo corriente sin saber de antemano cuántos objetos se han escrito, o qué tan grandes son.

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))

El ejemplo simula flujos usando dos buffers BytesIO. El primero recibe los objetos serializados, y su valor se alimenta a un segundo de el cual load() lee. Un simple formato de base de datos podría usar serialización para guardar objetos, también. El módulo shelve es una implementación.

$ python3 pickle_stream.py

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

Además de almacenar datos, la serialización es útil para la comunicación inter-proceso. Por ejemplo, os.fork() y os.pipe() pueden ser utilizados para establecer procesos de trabajo que leen instrucciones de trabajo de un pipe y escriben los resultados en otro pipe. El código básico para la gestión del grupo de trabajadores y el envío de trabajos y la recepción de respuestas pueden ser reutilizados, ya que los objetos de trabajo y respuesta no tienen que estar basados en una clase particular. Cuando utilices pipes o sockets, no olvides vaciar después de serializar cada objeto, para enviar los datos a través de la conexión al otro extremo. Ve el módulo multiprocessing para un gestor de trabajadores reutilizable.

Problemas al reconstruir objetos

Cuando se trabaja con clases personalizadas, la clase que está siendo serializada debe aparecer en el espacio de nombres del proceso leyendo la serialización. Sólo los datos de la instancia son serializados, no la definición de clase. El nombre de la clase es utilizado para encontrar el constructor para crear el nuevo objeto cuando se deserialice. El siguiente ejemplo escribe instancias de una clase en un archivo.

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)

Cuando se ejecuta, el script crea un archivo basado en el nombre dado como argumento en la línea de comando.

$ python3 pickle_dump_to_file_1.py test.dat

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

Un intento simplista de cargar los objetos serializados resultantes falla.

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))

Esta versión falla porque no hay una clase SimpleObject disponible.

$ python3 pickle_load_from_file_1.py test.dat

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

La versión corregida, que importa SimpleObject del script original, tiene éxito. Agregando esta declaración de importación al final de la lista de importación permite que el script encuentre la clase y construya el objeto.

from pickle_dump_to_file_1 import SimpleObject

Ejecutar el script modificado ahora produce los resultados deseados.

$ python3 pickle_load_from_file_2.py test.dat

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

Objetos no recuperables

No todos los objetos pueden ser serializados. Sockets, gestores de archivos, conexiones de bases de datos, y otros objetos con estado de ejecución que dependen del sistema operativo u otro proceso puede no ser posible guardar de manera significativa. Los objetos que tienen atributos no serializables pueden definir __getstate__() y __setstate__() para devolver un subconjunto del estado de la instancia a ser serializada.

El método __getstate__() debe devolver un objeto que contenga el estado interno del objeto. Una forma conveniente de representar ese estado es con un diccionario, pero el valor puede ser cualquier objeto serializable. El estado se almacena y se pasa a __setstate__() cuando el objeto se carga desde la serialización.

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}) (computed={!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)

Este ejemplo usa un objeto State separado para mantener el estado interno de MyClass. Cuando una instancia de MyClass se carga desde una serialización, __setstate__() se pasa una instancia State que se utiliza para inicializar el objeto.

$ python3 pickle_state.py

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

Advertencia

Si el valor devuelto es falso, entonces __setstate__() no se llama cuando el objeto se deserializa.

Referencias circulares

El protocolo pickle maneja automáticamente referencias circulares entre objetos, por lo que las estructuras de datos complejas no necesitan ningún manejo especial. Considera el grafo dirigido en the figure. Incluye varios ciclos, sin embargo, la estructura correcta puede ser serializada y luego deserializada.

digraph pickle_example {
  "root";
  "root" -> "a";
  "root" -> "b";
  "a" -> "b";
  "b" -> "a";
  "b" -> "c";
  "a" -> "a";
}

Serializando una estructura de datos con ciclos

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, seen=None, parent=None):
    """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)

Los nodos recargados no son el mismo objeto, pero la relación entre los nodos se mantiene y solo una copia del objeto con múltiples referencias es deserializado. Ambas declaraciones pueden ser verificado examinando los valores id() para los nodos antes y después de ser pasado por 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)

Ver también