Nivel de dificultad:4 sobre 5
El concepto de la serialización es simple. Tienes una estructura de datos en memoria que quieres grabar, reutilizar o enviar a alguien. ¿Cómo lo haces? Bueno, eso depende de lo que quieras grabar, de cómo lo quieras reutilizar y a quién se lo envías. Muchos juegos te permiten grabar el avance cuando sales de ellos, y continuar en donde lo dejaste cuando los vuelves a cargar (En realidad, esto también lo hacen las aplicaciones que no son de juegos). En estos casos, se necesita almacenar en disco una estructura de datos que almacena ''tu grado de avance hasta el momento'', cuando los juegos se reinician, es necesario volver a cargar estas estructuras de datos. Los datos, en estos casos, sólo se utilizan por el mismo programa que los creó, no se envían por la red ni se leen por nadie más que por el programa que los creó. Por ello, los posibles problemas de interoperabilidad quedan reducidos a asegurar que versiones posteriores del mismo programa pueden leer los datos escritos por versiones previas.
Para casos como estos, el módulo pickle es ideal. Forma parte de la librería estándar de Python, por lo que siempre está disponible. Es rápido, la mayor parte está escrito en C, como el propio intérprete de Python. Puede almacenar estructuras de datos de Python todo lo complejas que se necesite.
¿Qué puede almacenar el módulo pickle?
Si no es suficiente para ti, el módulo pickle se puede extender. Si estás interesado en la extensibilidad, revisa los enlaces de la sección de Lecturas recomendadas al final de este capítulo.
Este capítulo cuenta una historia con dos consolas de Python. Todos los ejemplos de este capítulo son parte de una única historia. Se te pedirá que vayas pasando de una consola a otra de Python para demostrar el funcionamiento de los módulos pickle y json.
Para ayudarte a mantener las cosas claras, abre la consola de Python y define la siguiente variable:
»> shell = 1 |
Mantén la ventana abierta. Ahora abre otra consola de Python y define la siguiente variable:
»> shell = 2 |
A lo largo de este capítulo, utilizaré la variable shell para indicar en qué consola de Python se ejecuta cada ejemplo.
El módulo pickle funciona con estructuras de datos. Vamos a construir una.
»> shell 1 »> entry = »> entry['title'] = 'Dive into history, 2009 edition' »> entry['article_link'] = 'http://diveintomark.org/' + 'archives/2009/03/27/dive-into-history-2009-edition' »> entry['comments_link'] = None »> entry['internal_id'] = b'548' »> entry['tags'] = ('diveintopython', 'docbook', 'html') »> entry['published'] = True »> import time »> entry['published_date'] = time.strptime('Fri Mar 27 22:20:42 2009') »> entry['published_date'] time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1) |
Bueno, ya tenemos un estupendo diccionario de Python. Vamos a salvarlo en un fichero.
»> shell 1 »> import pickle »> with open('entry.pickle', 'wb') as f: ... pickle.dump(entry, f) ... |
Esa última sentencia era muy importante.
Ahora cambia a la segunda consola de Python --la otra, la que no utilizaste para crear el diccionario entry.
»> shell 2 »> entry Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'entry' is not defined »> import pickle »> with open('entry.pickle', 'rb') as f: ... entry = pickle.load(f) ... »> entry 'comments_link': None, 'internal_id': b'548', 'title': 'Dive into history, 2009 edition', 'tags': ('diveintopython', 'docbook', 'html'), 'article_link': 'http://diveintomark.org/archives/2009/03/27/ dive-into-history-2009-edition', 'published_date': time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1), 'published': True |
El ciclo pickle.dump() / pickle.load() da como resultado una estructura de datos nueva que es igual a la original.
»> shell 1 »> with open('entry.pickle', 'rb') as f: ... entry2 = pickle.load(f) ... »> entry2 == entry True »> entry2 is entry False »> entry2['tags'] ('diveintopython', 'docbook', 'html') »> entry2['internal_id'] b'548' |
Los ejemplos de la sección anterior te mostraron cómo serializar un objeto Python directamente a un fichero en disco. Pero ¿qué sucede si no necesitas un fichero? Puedes serializar a un objeto bytes en memoria.
»> shell 1 »> b = pickle.dumps(entry) »> type(b) <class 'bytes'> »> entry3 = pickle.loads(b) »> entry3 == entry True |
El protocolo pickle existe desde hace muchos años, y ha madurado a la par que lo ha hecho Python. Por ello, actualmente existen cuatro versiones diferentes del protocolo.
Como puedes observar, la diferencia existente entre cadenas de texto y bytes vuelve a aparecer (si te sorprende es que no has estado poniendo atención). En la práctica, significa que mientras que Python 3 puede leer datos almacenados con el protocolo versión 2, Python 2 no puede leer datos almacenados con el protocolo versión 3.
¿A qué se parece el protocolo ``pickle''? Vamos a salir un momento de la consola de Python y echarle un vistazo al fichero entry.pickle que hemos creado.
you@localhost: /diveintopython3/examplesls - lentry.pickle - rw - r - - r - - 1youyou358Aug313 : 34entry.pickleyou@localhost : /diveintopython3/examples cat entry.pickle
comments_linkqNXtagsqXdiveintopythonqXdocbookqXhtmlq?qX publishedq?
XlinkXJhttp://diveintomark.org/archives/2009/03/27/dive-into-history-
2009-edition
q Xpublished_dateq
ctime
struct_time
?qRqXtitleqXDive into history, 2009 editionqu. |
No ha sido muy útil. Puedes ver las cadenas de texto, pero los otros tipos de dato salen como caracteres ilegibles. Los campos no están delimitados por tabuladores ni espacios. No se trata de un formato que quieras depurar por ti mismo.
»> shell 1 »> import pickletools »> with open('entry.pickle', 'rb') as f: ... pickletools.dis(f) 0: 80 PROTO 3 2: EMPTY_DICT 3: q BINPUT 0 5: ( MARK 6: X BINUNICODE 'published_date' 25: q BINPUT 1 27: c GLOBAL 'time struct_time' 45: q BINPUT 2 47: ( MARK 48: M BININT2 2009 51: K BININT1 3 53: K BININT1 27 55: K BININT1 22 57: K BININT1 20 59: K BININT1 42 61: K BININT1 4 63: K BININT1 86 65: J BININT -1 70: t TUPLE (MARK at 47) 71: q BINPUT 3 73: EMPTY_DICT 74: q BINPUT 4 76: 86 TUPLE2 77: q BINPUT 5 79: R REDUCE 80: q BINPUT 6 82: X BINUNICODE 'comments_link' 100: q BINPUT 7 102: N NONE 103: X BINUNICODE 'internal_id' 119: q BINPUT 8 121: C SHORT_BINBYTES 'xxxx' 127: q BINPUT 9 129: X BINUNICODE 'tags' 138: q BINPUT 10 140: X BINUNICODE 'diveintopython' 159: q BINPUT 11 161: X BINUNICODE 'docbook' 173: q BINPUT 12 175: X BINUNICODE 'html' 184: q BINPUT 13 186: 87 TUPLE3 187: q BINPUT 14 189: X BINUNICODE 'title' 199: q BINPUT 15 201: X BINUNICODE 'Dive into history, 2009 edition' 237: q BINPUT 16 239: X BINUNICODE 'article_link' |
256: q BINPUT 17 258: X BINUNICODE 'http://diveintomark.org/archives/2009/ 03/27/dive-into-history-2009-edition' 337: q BINPUT 18 339: X BINUNICODE 'published' 353: q BINPUT 19 355: 88 NEWTRUE 356: u SETITEMS (MARK at 5) 357: . STOP highest protocol among opcodes = 3 |
La información más interesante de este volcado es la que aparece en la última línea, ya que muestra la versión del protocolo de ``pickle'' con la que el fichero se grabó. No existe un marcador de versión explícito en el protocolo de ``pickle''. Para determinar la versión del protocolo, se observan los marcadores (códigos de operación - ``opcodes'') existentes en los datos almacenados y se utiliza el conocimiento expreso de qué códigos fueron introducidos en cada versión del protocolo ``pickle''. La función pickle.dis() hace exactamente eso e imprime el resultado en la última línea del volcado de salida.
La siguiente es una función que simplemente devuelve el número de versión sin imprimir nada:
import pickletools |
Y aquí la vemos en acción:
»> import pickleversion »> with open('entry.pickle', 'rb') as f: ... v = pickleversion.protocol_version(f) »> v 3 |
El formato utilizado por el módulo pickle es específico de Python. No intenta ser compatible con otros lenguajes de programación. Si la compatibilidad entre lenguajes es un requisito, necesitas utilizar otros formatos de serialización. Uno de ellos es JSON8.4. ``JSON'' significa ``JavaScript Object Notation - Notación de Objetos JavaScript'', pero no dejes que el nombre te engañe --JSON está diseñado explícitamente para permitir su uso en diferentes lenguajes de programación.
Python 3 incluye un módulo json en su librería estándar. Como el módulo pickle, el módulo json dispone de funciones para la serialización de estructuras de datos, almacenamiento de los datos serializados en disco, carga de los mismos y deserialización en un nuevo objeto Python. Pero también tiene importantes diferencias. La primera es que JSON es un formato de datos textual, no binario. La especificación RFC 46278.5 define el formato y cómo se codifican los diferentes tipos de datos de forma textual. Por ejemplo, un valor booleano se almacena como la cadena texto de cinco caracteres ``false'' o como la cadena de texto de cuatro caracteres ``true''. Todos los valores de JSON tienen en cuenta las mayúsculas y minúsculas.
Segundo, como cualquier formato basado en texto, existe el problema de los espacios en blanco. JSON permite el uso de un número arbitrario de espacios en blanco (espacios, tabuladores, retornos de carro y saltos de línea) entre los valores. Estos espacios en blanco son ``no significativos'', lo que significa que los codificadores de JSON pueden añadir tantos como deseen, y los decodificadores de JSON están obligados a ignorarlos siempre que se encuentren entre dos valores. Esto permite que los datos de un fichero JSON se puedan imprimir bien formateados, anidando de forma clara los valores que se encuentran dentro de otros para que puedas verlos bien en un editor o visor de texto estándar. El módulo json de Python dispone de opciones para codificar la salida con formato apropiado para la lectura.
Tercero, existe el problema perenne de la codificación de caracteres. Puesto que JSON codifica los valores como texto plano, pero como ya sabes, no existe tal ``texto plano''. JSON debe almacenarse con caracteres Unicode (UTF-32, UTF-16 o, por defecto, UTF-8), y la sección 3 de la RFC-46278.6, define cómo indicar qué codificación se está utilizando.
JSON se parece mucho a una estructura de datos que pudieras definir en JavaScript. No es casualidad, en realidad, puedes utilizar la función eval() de JavaScript para ``decodificar'' los datos serializados en JSON. Lo fundamental es conocer que JSON forma parte del propio lenguaje JavaScript. Como tal, JSON puede que ya te sea familiar.
»> shell 1 »> basic_entry = »> basic_entry['id'] = 256 »> basic_entry['title'] = 'Dive into history, 2009 edition' »> basic_entry['tags'] = ('diveintopython', 'docbook', 'html') »> basic_entry['published'] = True »> basic_entry['comments_link'] = None »> import json »> with open('basic.json', mode='w', encoding='utf-8') as f: ... json.dump(basic_entry, f) |
¿Cómo queda el resultado serializado?
you@localhost: /diveintopython3/examples> cat basic.json {"published": true, "tags": ["diveintopython", "docbook", "html"], "comments_link": null, "id": 256, "title": "Dive into history, 2009 edition"} |
»> shell 1 »> with open('basic-pretty.json', mode='w', encoding='utf-8') as f: ... json.dump(basic_entry, f, indent=2) |
Si se pasa el parámetro indent a la función json.dump() el fichero JSON resultante será más legible aún. A costa de un fichero de tamaño mayor. El parámetro indent es un valor entero en el que 0 significa ``pon cada valor en su propia línea'' y un número mayor que cero significa ``pon cada valor en su propia línea, y utiliza este número de espacios para indentar las estructuras de datos anidadas''.
Por lo que éste es el resultado:
you@localhost: /diveintopython3/examples> cat basic-pretty.json { "published": true, "tags": [ "diveintopython", "docbook", "html" ], "comments_link": null, "id": 256, "title": "Dive into history, 2009 edition" } |
Puesto que JSON no es específico de Python, existen algunas diferencias en su cobertura de los tipos de dato de Python. Algunas de ellas son simplemente de denominación, pero existen dos tipos de dato importantes de Python que no existen en JSON. Observa esta tabla a ver si los echas de menos:
Notas | JSON | Python 3 |
object | dictionary | |
array | list | |
string | string | |
integer | integer | |
real number | float | |
* | true | True |
* | false | False |
* | null | None |
* Las mayúsculas y minúsculas en los valores JSON son significativas. |
¿Te has dado cuenta de lo que falta? ¡Tuplas y bytes! JSON tiene un tipo de datos array, al que se mapean las listas de Python, pero no tiene un tipo de datos separado para los ``arrays congelados'' (tuplas). Y aunque JSON soporta cadenas de texto, no tiene soporte para los objetos bytes o arrays de bytes.
Incluso aunque JSON no tiene soporte intrínseco de bytes, es posible serializar objetos bytes. El módulo json proporciona unos puntos de extensibilidad para codificar y decodificar tipos de dato desconocidos (Por desconocido se entiende en este contexto a aquellos tipos de datos que no están definidos en la especificación de JSON). Si quieres codificar bytes u otros tipos de datos que JSON no soporte de forma nativa, necesitas proporcionar codificadores de decodificadores a medida para esos tipos de dato.
»> shell 1 »> entry 'comments_link': None, 'internal_id': b'548', 'title': 'Dive into history, 2009 edition', 'tags': ('diveintopython', 'docbook', 'html'), 'article_link': 'http://diveintomark.org/archives/2009/03 /27/dive-into-history-2009-edition', 'published_date': time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1), 'published': True »> import json »> with open('entry.json', 'w', encoding='utf-8') as f: ... json.dump(entry, f) ... Traceback (most recent call last): File "<stdin>", line 5, in <module> File "C:31__init__.py", line 178, in dump for chunk in iterable: File "C:31.py", line 408, in _iterencode for chunk in _iterencode_dict(o, _current_indent_level): File "C:31.py", line 382, in _iterencode_dict for chunk in chunks: File "C:31.py", line 416, in _iterencode o = _default(o) File "C:31.py", line 170, in default raise TypeError(repr(o) + " is not JSON serializable") TypeError: b'548' is not JSON serializable |
Lo que ha pasado es que la función json.dump() intentó serializar el objeto bytes pero falló porque JSON no dispone de soporte de objetos bytes. Sin embargo, si es importante almacenar bytes en este formato, puedes definir tu propio ``formato de serialización''.
def to_json(python_object): if isinstance(python_object, bytes): return '__class__': 'bytes', '__value__': list(python_object) raise TypeError(repr(python_object) + ' is not JSON serializable') |
Y eso es todo, no necesitas hacer nada más. En particular, esta función a medida retorna un un diccionario de Python, no una cadena. No estás haciendo la serialización a JSON completa por ti mismo; solamente la parte correspondiente a un tipo de datos que no está soportado de forma nativa. La función json.dump() hará el resto.
»> shell 1 »> import customserializer »> with open('entry.json', 'w', encoding='utf-8') as f: ... json.dump(entry, f, default=customserializer.to_json) ... Traceback (most recent call last): File "<stdin>", line 9, in <module> json.dump(entry, f, default=customserializer.to_json) File "C:31__init__.py", line 178, in dump for chunk in iterable: File "C:31.py", line 408, in _iterencode for chunk in _iterencode_dict(o, _current_indent_level): File "C:31.py", line 382, in _iterencode_dict for chunk in chunks: File "C:31.py", line 416, in _iterencode o = _default(o) File "/Users/pilgrim/diveintopython3/examples/customserializer.py", line 12, in to_json raise TypeError(repr(python_object) + ' is not JSON serializable') TypeError: time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1) is not JSON serializable |
Aunque obtener una excepción diferente podría no parecer mucho progreso ¡lo es! Haremos una modificación más para superar este error:
import time |
Con estas dos conversiones a medida, la estructura completa de datos entry debería serializarse a JSON sin más problemas.
»> shell 1 »> with open('entry.json', 'w', encoding='utf-8') as f: ... json.dump(entry, f, default=customserializer.to_json) ... |
you@localhost: /diveintopython3/examplesls - lexample.json - rw - r - - r - - 1youyou391Aug313 : 34entry.jsonyou@localhost : /diveintopython3/examples cat example.json
"published_date": "__class__": "time.asctime",
"__value__": "Fri Mar 27 22:20:42 2009",
"comments_link": null, "internal_id": "__class__": "bytes",
"__value__": [222, 213, 180, 248],
"tags": ["diveintopython", "docbook", "html"],
"title": "Dive into history, 2009 edition",
"article_link": "http://diveintomark.org/archives/
2009/03/27/dive-into-history-2009-edition",
"published": true |
Como el módulo pickle, el módulo json tiene una función load() que toma un objeto de flujo de datos y lee la información formateada en JSON y crea un objeto Python que es idéntico a la estructura de datos JSON.
»> shell 2 »> del entry »> entry Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'entry' is not defined »> import json »> with open('entry.json', 'r', encoding='utf-8') as f: ... entry = json.load(f) ... »> entry 'comments_link': None, 'internal_id': '__class__': 'bytes', '__value__': [222, 213, 180, 248], 'title': 'Dive into history, 2009 edition', 'tags': ['diveintopython', 'docbook', 'html'], 'article_link': 'http://diveintomark.org/archives/ 2009/03/27/dive-into-history-2009-edition', 'published_date': '__class__': 'time.asctime', '__value__': 'Fri Mar 27 22:20:42 2009', 'published': True |
La función json.load() no sabe nada sobre ninguna función de conversión que puedas haber pasado a la función json.dump(). Lo que se necesita es la función opuesta a to_json() --una función que tomará un objeto JSON convertido a medida y convertirá de nuevo a Python el tipo de datos original.
# add this to customserializer.py def from_json(json_object): if '__class__' in json_object: if json_object['__class__'] == 'time.asctime': return time.strptime(json_object['__value__']) if json_object['__class__'] == 'bytes': return bytes(json_object['__value__']) return json_object |
Eso es todo. Solamente se manejaban dos tipos de dato en la función to_json(), y ahora son esos dos tipos de dato los que se manejan en la función from_json(). Este es el resultado:
»> shell 2 »> import customserializer »> with open('entry.json', 'r', encoding='utf-8') as f: ... entry = json.load(f, object_hook=customserializer.from_json) ... »> entry 'comments_link': None, 'internal_id': b'548', 'title': 'Dive into history, 2009 edition', 'tags': ['diveintopython', 'docbook', 'html'], 'article_link': 'http://diveintomark.org/archives/ 2009/03/27/dive-into-history-2009-edition', 'published_date': time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1), 'published': True |
Sin embargo, aún queda un pequeño tema por tratar.
»> shell 1 »> import customserializer »> with open('entry.json', 'r', encoding='utf-8') as f: ... entry2 = json.load(f, object_hook=customserializer.from_json) ... »> entry2 == entry False »> entry['tags'] ('diveintopython', 'docbook', 'html') »> entry2['tags'] ['diveintopython', 'docbook', 'html'] |
Muchos artículos del módulo pickle hacen referencia a cPickle. En Python 2 existen dos implementaciones del módulo pickle, uno escrito en Python puro y otro escrito en C (pero que se puede llamar desde Python). En Python 3 se han consolidado ambos módulos, por lo que siempre deberías utilizar import pickle.
Sobre el módulo pickle:
Sobre el módulo json:
Sobre la extensibilidad de pickle:
José Miguel González Aguilera 2016-08-18