Serialización y deserialización de objetos Python: Parte 1
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.



