1. Code
  2. Python

Serialización y deserialización de objetos Python: Parte 1

La serialización y deserialización de objetos Python es un aspecto importante de cualquier programa no trivial. Si guardas algo en un archivo en Python, si lees un archivo de configuración o si respondes a una solicitud HTTP, puedes serializar y deserializar objetos.
Scroll to top

Spanish (Español) translation by Andrea Jiménez (you can also view the original English article)

La serialización y deserialización de objetos Python es un aspecto importante de cualquier programa no trivial. Si guardas algo en un archivo en Python, si lees un archivo de configuración o si respondes a una solicitud HTTP, puedes serializar y deserializar objetos.

En cierto sentido, la serialización y la deserialización son las cosas más aburridas del mundo. ¿A quién le importan todos los formatos y protocolos? Solo deseas conservar o transmitir algunos objetos de Python y recuperarlos intactos más tarde.

Esta es una forma muy saludable de ver el mundo a nivel conceptual. Pero, a nivel pragmático, el esquema, formato o protocolo de serialización que elijas puede determinar qué tan rápido se ejecuta tu programa, qué tan seguro es, cuánta libertad tienes para mantener tu estado y qué tan bien vas a interoperar con otros sistemas.

La razón por la que hay tantas opciones es que diferentes circunstancias requieren diferentes soluciones. No hay "un tamaño que se adapte a todos". En este tutorial de dos partes, repasaré los pros y los contras de los esquemas de serialización y deserialización más exitosos, mostraré cómo usarlos y proporcionaré pautas para elegir entre ellos cuando se enfrente a un caso de uso específico.

Ejemplo de ejecución

En las siguientes secciones, serializaré y deserializaré los mismos gráficos de objetos de Python usando diferentes serializadores. Para evitar la repetición, definiré estos gráficos de objetos aquí.

Gráfico de objetos simple

El gráfico de objetos simple es un diccionario que contiene una lista de números enteros, una cadena, un float, un booleano y un None.

1
simple = dict(int_list=[1, 2, 3],
2
3
              text='string',
4
5
              number=3.44,
6
7
              boolean=True,
8
9
              none=None) 

Gráfico de objetos complejo

El gráfico de objetos complejo también es un diccionario, pero contiene un objeto datetime y una instancia de clase definida por el usuario que tiene un atributo self.simple, que se configura en el gráfico de objetos simple.

1
from datetime import datetime
2
3
4
5
class A(object):
6
7
    def __init__(self, simple):
8
9
        self.simple = simple        
10
11
    def __eq__(self, other):
12
13
        if not hasattr(other, 'simple'):
14
15
            return False
16
17
        return self.simple == other.simple
18
19
    def __ne__(self, other):
20
21
        if not hasattr(other, 'simple'):
22
23
            return True
24
25
        return self.simple != other.simple
26
27
28
29
complex = dict(a=A(simple), when=datetime(2016, 3, 7))

Pickle

Pickle es un elemento básico. Es un formato de serialización de objetos nativo de Python. La interfaz pickle proporciona cuatro métodos: dump, dumps, load, y loads. El método dump() se serializa en un archivo abierto (objeto similar a un archivo). El método dumps() se serializa en una cadena. El método load() se deserializa a partir de un objeto abierto similar a un archivo. El método loads() se deserializa a partir de una cadena.

Pickle admite de forma predeterminada un protocolo textual, pero también tiene un protocolo binario, que es más eficiente, pero no legible por humanos (útil al depurar).

Así es como se selecciona un gráfico de objeto de Python en una cadena y en un archivo usando ambos protocolos.

1
import cPickle as pickle
2
3
4
5
pickle.dumps(simple)
6
7
"(dp1\nS'text'\np2\nS'string'\np3\nsS'none'\np4\nNsS'boolean'\np5\nI01\nsS'number'\np6\nF3.4399999999999999\nsS'int_list'\np7\n(lp8\nI1\naI2\naI3\nas."
8
9
10
11
pickle.dumps(simple, protocol=pickle.HIGHEST_PROTOCOL)
12
13
'\x80\x02}q\x01(U\x04textq\x02U\x06stringq\x03U\x04noneq\x04NU\x07boolean\x88U\x06numberq\x05G@\x0b\x85\x1e\xb8Q\xeb\x85U\x08int_list]q\x06(K\x01K\x02K\x03eu.'

La representación binaria puede parecer más grande, pero esto es una ilusión debido a su presentación. Cuando se realiza un volcado en un archivo, el protocolo textual es de 130 bytes, mientras que el protocolo binario es de solo 85 bytes.

1
pickle.dump(simple, open('simple1.pkl', 'w'))
2
3
pickle.dump(simple, open('simple2.pkl', 'wb'), protocol=pickle.HIGHEST_PROTOCOL)
4
5
6
7
ls -la sim*.*
8
9
-rw-r--r--  1 gigi  staff  130 Mar  9 02:42 simple1.pkl
10
11
-rw-r--r--  1 gigi  staff   85 Mar  9 02:43 simple2.pkl

Descomprimir una cadena es tan simple como:

1
x = pickle.loads("(dp1\nS'text'\np2\nS'string'\np3\nsS'none'\np4\nNsS'boolean'\np5\nI01\nsS'number'\np6\nF3.4399999999999999\nsS'int_list'\np7\n(lp8\nI1\naI2\naI3\nas.")
2
3
assert x == simple
4
5
6
7
x = pickle.loads('\x80\x02}q\x01(U\x04textq\x02U\x06stringq\x03U\x04noneq\x04NU\x07boolean\x88U\x06numberq\x05G@\x0b\x85\x1e\xb8Q\xeb\x85U\x08int_list]q\x06(K\x01K\x02K\x03eu.')
8
9
assert x == simple

Ten en cuenta que pickle puede descubrir el protocolo automáticamente. No es necesario especificar un protocolo ni siquiera para el binario.

Descomprimir un archivo es igual de fácil. Solo necesitas proporcionar un archivo abierto.

1
x = pickle.load(open('simple1.pkl'))
2
3
assert x == simple
4
5
6
7
x = pickle.load(open('simple2.pkl'))
8
9
assert x == simple
10
11
12
13
x = pickle.load(open('simple2.pkl', 'rb'))
14
15
assert x == simple

Según la documentación, se supone que debes abrir pickles binarios usando el modo 'rb', pero como puedes ver, funciona de cualquier manera.

Veamos cómo pickle maneja el gráfico de objetos complejo.

1
pickle.dumps(complex)
2
3
"(dp1\nS'a'\nccopy_reg\n_reconstructor\np2\n(c__main__\nA\np3\nc__builtin__\nobject\np4\nNtRp5\n(dp6\nS'simple'\np7\n(dp8\nS'text'\np9\nS'string'\np10\nsS'none'\np11\nNsS'boolean'\np12\nI01\nsS'number'\np13\nF3.4399999999999999\nsS'int_list'\np14\n(lp15\nI1\naI2\naI3\nassbsS'when'\np16\ncdatetime\ndatetime\np17\n(S'\\x07\\xe0\\x03\\x07\\x00\\x00\\x00\\x00\\x00\\x00'\ntRp18\ns."
4
5
6
7
pickle.dumps(complex, protocol=pickle.HIGHEST_PROTOCOL)
8
9
'\x80\x02}q\x01(U\x01ac__main__\nA\nq\x02)\x81q\x03}q\x04U\x06simpleq\x05}q\x06(U\x04textq\x07U\x06stringq\x08U\x04noneq\tNU\x07boolean\x88U\x06numberq\nG@\x0b\x85\x1e\xb8Q\xeb\x85U\x08int_list]q\x0b(K\x01K\x02K\x03eusbU\x04whenq\x0ccdatetime\ndatetime\nq\rU\n\x07\xe0\x03\x07\x00\x00\x00\x00\x00\x00\x85Rq\x0eu.'
10
11
12
13
pickle.dump(complex, open('complex1.pkl', 'w'))
14
15
pickle.dump(complex, open('complex2.pkl', 'wb'), protocol=pickle.HIGHEST_PROTOCOL)
16
17
18
19
ls -la comp*.*
20
21
-rw-r--r--  1 gigi  staff  327 Mar  9 02:58 complex1.pkl
22
23
-rw-r--r--  1 gigi  staff  171 Mar  9 02:58 complex2.pkl

La eficiencia del protocolo binario es aún mayor con gráficos de objetos complejos.

JSON

JSON (JavaScript Object Notation) forma parte de la biblioteca estándar de Python desde Python 2.5. Lo consideraré un formato nativo en este momento. Es un formato basado en texto y es el rey no oficial de la web en lo que respecta a la serialización de objetos. Su sistema de tipos presenta naturalmente JavaScript, por lo que es bastante limitado.

Serialicemos y deserialicemos los gráficos de objetos simples y complejos y veamos qué sucede. La interfaz es casi idéntica a la interfaz pickle. Tienes funciones dump(), dumps(), load() y loads(). Pero no hay protocolos para seleccionar y hay muchos argumentos opcionales para controlar el proceso. Comencemos de manera simple volcando el gráfico de objeto simple sin ningún argumento especial:

1
import json
2
3
print json.dumps(simple)
4
5
{"text": "string", "none": null, "boolean": true, "number": 3.44, "int_list": [1, 2, 3]}

La salida se ve bastante legible, pero no hay indentación. Para un gráfico de objetos más grande, esto puede ser un problema. Indentemos la salida:

1
print json.dumps(simple, indent=4)
2
3
{
4
5
    "text": "string",
6
7
    "none": null,
8
9
    "boolean": true,
10
11
    "number": 3.44,
12
13
    "int_list": [
14
15
        1,
16
17
        2,
18
19
        3
20
21
    ]
22
23
}

Se ve mucho mejor. Pasemos al gráfico de objetos complejo.

1
json.dumps(complex)
2
3
---------------------------------------------------------------------------
4
5
TypeError                                 Traceback (most recent call last)
6
7
<ipython-input-19-1be2d89d5d0d> in <module>()
8
9
----> 1 json.dumps(complex)
10
11
12
13
/usr/local/Cellar/python/2.7.10/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/__init__.pyc in dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, encoding, default, sort_keys, **kw)
14
15
    241         cls is None and indent is None and separators is None and
16
17
    242         encoding == 'utf-8' and default is None and not sort_keys and not kw):
18
19
--> 243         return _default_encoder.encode(obj)
20
21
    244     if cls is None:
22
23
    245         cls = JSONEncoder
24
25
26
27
/usr/local/Cellar/python/2.7.10/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.pyc in encode(self, o)
28
29
    205         # exceptions aren't as detailed.  The list call should be roughly

30
31
    206         # equivalent to the PySequence_Fast that ''.join() would do.

32
33
--> 207         chunks = self.iterencode(o, _one_shot=True)
34
35
    208         if not isinstance(chunks, (list, tuple)):
36
37
    209             chunks = list(chunks)
38
39
40
41
/usr/local/Cellar/python/2.7.10/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.pyc in iterencode(self, o, _one_shot)
42
43
    268                 self.key_separator, self.item_separator, self.sort_keys,
44
45
    269                 self.skipkeys, _one_shot)
46
47
--> 270         return _iterencode(o, 0)
48
49
    271
50
51
    272 def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
52
53
54
55
/usr/local/Cellar/python/2.7.10/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.pyc in default(self, o)
56
57
    182
58
59
    183         """

60


61
--> 184         raise TypeError(repr(o) + " is not JSON serializable")

62


63
    185

64


65
    186     def encode(self, o):

66


67


68


69
TypeError: <__main__.A object at 0x10f367cd0> is not JSON serializable

¡Wow! No se ve nada bien. ¿Qué pasó? El mensaje de error es que el objeto A no es serializable JSON. Recuerda que JSON tiene un sistema de tipos muy limitado y no puede serializar clases definidas por el usuario automáticamente. La forma de abordarlo es subclasificar la clase JSONEncoder utilizada por el módulo json e implementar el valor predeterminado default() que se llama cada vez que el codificador JSON se encuentra con un objeto que no puede serializar.

El trabajo del codificador personalizado es convertirlo en un gráfico de objeto Python que el codificador JSON pueda codificar. En este caso tenemos dos objetos que requieren una codificación especial: el objeto datetime y la clase A. El siguiente codificador hace el trabajo. Cada objeto especial se convierte en un dict donde la clave es el nombre del tipo rodeado de dunders (guiones bajos dobles). Esto será importante para la decodificación.

1
from datetime import datetime
2
3
import json
4
5
6
7
8
9
class CustomEncoder(json.JSONEncoder):
10
11
     def default(self, o):
12
13
         if isinstance(o, datetime):
14
15
             return {'__datetime__': o.replace(microsecond=0).isoformat()}
16
17
         return {'__{}__'.format(o.__class__.__name__): o.__dict__}

Intentemos de nuevo con nuestro codificador personalizado:

1
serialized = json.dumps(complex, indent=4, cls=CustomEncoder)
2
3
print serialized
4
5
6
7
{
8
9
    "a": {
10
11
        "__A__": {
12
13
            "simple": {
14
15
                "text": "string",
16
17
                "none": null,
18
19
                "boolean": true,
20
21
                "number": 3.44,
22
23
                "int_list": [
24
25
                    1,
26
27
                    2,
28
29
                    3
30
31
                ]
32
33
            }
34
35
        }
36
37
    },
38
39
    "when": {
40
41
        "__datetime__": "2016-03-07T00:00:00"
42
43
    }
44
45
}

Esto es hermoso. El gráfico de objeto complejo se serializó correctamente y la información de tipo original de los componentes se conservó mediante las claves: "__A__" y "__datetime__". Si usas dunders para tus nombres, entonces debes idear una convención diferente para denotar tipos especiales.

Decodifiquemos el gráfico de objetos complejo.

1
> deserialized = json.loads(serialized)
2
3
> deserialized == complex
4
5
False

Mmm, la deserialización funcionó (sin errores), pero es diferente al gráfico de objeto complejo original que serializamos. Algo anda mal. Veamos el gráfico de objetos deserializados. Usaré la función pprint del módulo pprint para una impresión bonita.

1
> from pprint import pprint
2
3
> pprint(deserialized)
4
5
{u'a': {u'__A__': {u'simple': {u'boolean': True,
6
7
                               u'int_list': [1, 2, 3],
8
9
                               u'none': None,
10
11
                               u'number': 3.44,
12
13
                               u'text': u'string'}}},
14
15
 u'when': {u'__datetime__': u'2016-03-07T00:00:00'}}

De acuerdo. El problema es que el módulo json no sabe nada sobre la clase A o incluso el objeto estándar de fecha y hora. Simplemente deserializa todo de forma predeterminada en el objeto Python que coincide con su sistema de tipos. Para volver a un gráfico de objetos de Python enriquecido, necesitas una decodificación personalizada.

No hay necesidad de una subclase de decodificadores personalizados. Las funciones load() y loads() proporcionan el parámetro "object_hook" que te permite proporcionar una función personalizada que convierte dicts en objetos.

1
def decode_object(o):
2
3
    if '__A__' in o:
4
5
        a = A()
6
7
        a.__dict__.update(o['__A__'])
8
9
        return a
10
11
    elif '__datetime__' in o:
12
13
        return datetime.strptime(o['__datetime__'], '%Y-%m-%dT%H:%M:%S')        
14
15
    return o

Decodifiquemos usando la función decode_object() como un parámetro para el parámetro loads() object_hook.

1
> deserialized = json.loads(serialized, object_hook=decode_object)
2
3
> print deserialized
4
5
{u'a': <__main__.A object at 0x10d984790>, u'when': datetime.datetime(2016, 3, 7, 0, 0)}
6
7
8
9
> deserialized == complex
10
11
True

Conclusión

En la primera parte de este tutorial, aprendiste sobre el concepto general de serialización y deserialización de objetos Python y exploraste las entradas y salidas de la serialización de objetos Python usando Pickle y JSON.

En la segunda parte, aprenderás sobre YAML, problemas de rendimiento y seguridad y una revisión rápida de esquemas de serialización adicionales.