Apartados


8. Serialización de Objetos en Python

Nivel de dificultad:4 sobre 5

``Desde que vivimos en este apartamento, cada sábado me he levantado a las 6:15,
me he preparado un tazón de cereales con leche,
me he sentado en este lado de este sofá, he puesto la BBC America, y he visto Doctor Who.''
--Sheldon, La teoría del Big Bang.8.1

8.1 Inmersión

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.

8.1.1 Una nota breve sobre los ejemplos 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.

8.2 Almacenamiento de datos a un fichero ``pickle''

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)

  1. Línea 1: Tecléalo en la consola #1.

  2. Línea 3: La idea aquí es construir un diccionario de Python que pueda representar algo que sea útil, como una entrada de una fuente Atom. Pero también quiero asegurarme de que contiene diferentes tipos de datos para mostrar el funcionamiento del módulo pickle. No entres demasiado en los valores concretos.

  3. Línea 13: El módulo time contiene una estructura de datos (time_struct) que representa un punto en el tiempo (con una precisión de milisegundo) y funciones que sirven para manipular estructuras de este tipo. La función strptime() recibe una cadena formateada y la convierte en una estructura time_struct. Esta cadena se encuentra en el formato por defecto, pero puedes controlarlo con los códigos de formato. Para más detalles consulta el módulo time8.3.

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

  1. Línea 1: Seguimos en la consola #1.

  2. Línea 4: Utiliza la función open() para abrir un fichero. El modo de apertura es 'wb', de escritura y en binario. Lo envolvemos en una sentencia with para asegurar que el fichero se cierra automáticamente al finalizar su uso.

  3. Línea 5: La función dump() del módulo pickle toma una estructura de datos serializable de Python y la serializa a un formato binario, específico de Python, y almacena el resultado en el fichero abierto.

Esa última sentencia era muy importante.

8.3 Carga de datos de un fichero ``pickle''

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

  1. Línea 1: Consola #2.

  2. Línea 3: La variable entry no está definida en esta consola. La definimos en la consola #1, que se trata de un entorno totalmente separado de éste y tiene su propio estado.

  3. Línea 8: Se abre el fichero entry.pickle que creamos con la consola #1. El módulo pickle utiliza un formato binario, por lo que siempre hay que abrir los ficheros de este tipo en modo binario.

  4. Línea 9: La función pickle.load() toma un objeto stream, lee los datos serializados del stream, crea un nuevo objeto Python, recrea los datos serializados en el nuevo objeto Python y devuelve el objeto.

  5. Línea 11: Ahora la variable entry contiene un diccionario con las claves y valores que nos son familiares de la otra consola.

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'

  1. Línea 1: Volvemos a la consola #1.

  2. Línea 3: Abrimos el fichero entry.pickle.

  3. Línea 4: Cargamos los datos serializados en la nueva variable entry2.

  4. Línea 6: Python confirma que los dos diccionarios, entry y entry2, son iguales. En esta consola, construimos el diccionario almacenado en entry desde cero, creando un diccionario vacío y añadiéndole valores poco a poco. Serializamos el diccionario y lo almacenamos en el fichero entry.pickle. Ahora hemos recuperado los datos serializados desde el fichero y hemos creado una réplica perfecta de la estructura de datos original.

  5. Línea 8: Igualdad no es lo mismo que identidad. Como he dicho, hemos creado una réplica perfecta de los datos originales. Pero son una copia.

  6. Línea 10: Por razones que aclararé más tarde en el capítulo, he querido mostrar que el valor de la clave 'tags' es una tupla, y el valor de la clave 'internal_id' es un objeto bytes.

8.4 Serialización con ``pickle'' sin pasar por un fichero

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

  1. Línea 3: La función pickle.dumps() (observa la 's' al final del nombre de la función) realiza la misma serialización que la función pickle.dump(). Pero en lugar de tomar como parámetro un objeto stream y serializar sobre él, simplemente retorna los datos serializados.

  2. Línea 4: Puesto que el protocolo pickle utiliza un formato de datos binario, la función pickle.dumps() retorna un objeto bytes.

  3. Línea 6: La función pickle.loads() (de nuevo, observa la 's' al final del nombre de la función) realiza la misma operación que la función pickle.load(). Pero en lugar de tomar como parámetro un objeto stream y leer de él los datos, toma un objeto bytes que contenga datos serializados.

  4. Línea 7: El resultado final es el mismo: una réplica perfecta del diccionario original.

8.5 Los bytes y las cadenas de nuevo vuelven sus feas cabezas

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.

8.6 Depuración de ficheros ``pickle''

¿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

def protocol_version(file_object): maxproto = -1 for opcode, arg, pos in pickletools.genops(file_object): maxproto = max(maxproto, opcode.proto) return maxproto

Y aquí la vemos en acción:

»> import pickleversion
»> with open('entry.pickle', 'rb') as f:
...     v = pickleversion.protocol_version(f)
»> v
3

8.7 Serialización de objetos Python para cargarlos en otros lenguajes

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.

8.8 Almacenamiento de datos en un fichero JSON

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)

  1. Línea 3: Vamos a crear una nueva estructura de datos, en lugar de reutilizar la estructura de datos entry preexistente. Después veremos qué sucede cuando intentamos codificar en JSON la otra estructura de datos más compleja.

  2. Línea 10: JSON está basado en texto, lo que significa que es necesario abrir el fichero en modo texto y especificar una codificación de caracteres. Nunca te equivocarás utilizando UTF-8.

  3. Línea 11: Como con el módulo pickle, el módulo json define la función dump() que toma una estructura de datos Python y un objeto de flujo (stream) con permisos de escritura. La función dump() serializa la estructura de datos de Python y escribe el resultado en el objeto de flujo. Al hacerlo dentro de una sentencia with nos aseguramos de que el fichero quede cerrado correctamente cuando hayamos terminado.

¿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"}
Es más legible que el fichero en formato de pickle. Pero como JSON puede contener tantos espacios en blanco como se desee entre diferentes valores, y el módulo json proporciona una forma sencilla de utilizar esta capacidad, podemos crear ficheros JSON aún más legibles.

»> 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"
}

8.9 Mapeo de los tipos de datos de Python a JSON

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.

8.10 Serialización de tipos no soportados en JSON

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

  1. Línea 3: Ok, es el momento de volver a la estructura de datos entry. Tiene de todo: un valor booleano, un None, una cadena de texto, una tupla de cadenas de texto, un objeto bytes y una estructura time.

  2. Línea 15: Sé lo que he dicho antes, pero vamos a repetirlo: JSON es un formato de texto. Siempre se deben abrir los ficheros JSON en modo texto con la codificación de caracteres UTF-8.

  3. Línea 18: ¡Error! ¿Qué ha pasado?

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

  1. Línea 1: Para definir un formato de serialización para un tipo de datos que JSON no soporte de forma nativa, simplemente define una función que tome un objeto Python como parámetro. Este objeto será el que la función json.dump() sea incapaz de serializar de forma nativa --en este caso el objeto bytes.

  2. Línea 2: La función debe validar el tipo de datos que recibe. No es estrictamente necesario pero así queda totalmente claro que casos cubre esta función, y hace más sencillo ampliarla más tarde.

  3. Línea 4: En este caso, he elegido convertir el objeto bytes en un diccionario. La clave __class__ guardará el tipo de datos original (como una cadena, ``bytes''), y la clave __value__ guardará el valor real. Como hay que convertirlo a algo que pueda serializarse en JSON, no se puede guardar directamente el objeto bytes. Como un objeto bytes es una secuencia de números enteros; con cada entero entre el 0 y el 255, podemos utilizar la función list() para convertir el objeto bytes en una lista de enteros. De forma que el objeto b'\xDE\xD5\x84\xF8' se convierte en [222, 213, 180, 248]. Por ejemplo, el byte \xDE en hexadecimal, se convierte en 222 en decimal, \xD5 es 213 y así cada uno de ellos.

  4. Línea 5: Esta línea es importante. La estructura de datos que estás serializando puede contener tipos de dato que ni el serializador interno del módulo de Python ni el tuyo puedan manejar. En este caso, tu serializador debe elevar una excepción TypeError para que la función json.dump() sepa que tu serializador no reconoció el tipo de dato del objeto.

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

  1. Línea 3: El módulo customserializer es el lugar en el que has definido la función to_json() del ejemplo anterior.

  2. Línea 4: El modo texto, la codificación UTF-8, etc. ¡Lo olvidarás! ¡a veces lo olvidarás! Y todo funcionará hasta el momento en que falle, y cuando falle, lo hará de forma espectacular.

  3. Línea 5: Este es el trozo importante: asignar una función de conversión ad-hoc en la función json.dump(), hay que pasar tu función a la función json.dump() en el parámetro default.

  4. Línea 20: Ok, realmente no ha funcionado. Pero observa la excepción. La función json.dump() ya no se queja más sobre el objeto de tipo bytes. Ahora se está quejando sobre un objeto totalmente diferente, el objeto time.struct_time.

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

def to_json(python_object): if isinstance(python_object, time.struct_time): return '__class__': 'time.asctime', '__value__': time.asctime(python_object) if isinstance(python_object, bytes): return '__class__': 'bytes', '__value__': list(python_object) raise TypeError(repr(python_object) + ' is not JSON serializable')

  1. Línea 4: Añadimos código a nuestra función customserializer.to_json(), necesitamos validar que el objeto Python sea time.struct_time (aquél con el que la función json.dump() está teniendo problemas).

  2. Línea 6: Haremos una conversión parecida a la que hicimos con el objeto bytes: convertir el objeto time.struct_time en un diccionario que solamente contenga valores serializables en JSON. En este caso, la forma más sencilla de convertir una fecha/hora a JSON es convertirlo en una cadena con la función time.asctime(). La función time.asctime() convertirá la estructura en la cadena 'Fri Mar 27 22:20:42 2009'.

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

8.11 Carga de datos desde un fichero JSON

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

  1. Línea 3: Con fines demostrativos, pasamos a la consola #2 y borramos la estructura de datos entry que habíamos creado antes con el módulo pickle.

  2. Línea 10: En el caso más simple, la función json.load() funciona de la misma forma que la función pickle.load(). Le pasamos un flujo de datos y devuelve un objeto Python nuevo.

  3. Línea 12: Tengo buenas y malas noticias. Las buenas primero: la función json.load() carga satisfactoriamente el ficheroentry.json que has creado en la consola #1 y crea un nuevo objeto Python que contiene la información. Ahora las malas noticias: No recrea la estructura de datos entry original. Los dos valores 'internal_id' y 'published_date' se han recreado como diccionarios --específicamente, los diccionarios con valores compatibles JSON que creamos en la función de conversión to_json().

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

  1. Línea 2: Esta función de conversión también toma un parámetro y devuelve un valor. Pero el parámetro que toma no es una cadena, es un objeto Python --el resultado de deserializar la cadena JSON en un objeto Python.

  2. Línea 3: Lo único que hay que hacer es validar si el objeto contiene la clave '__class__' que creó la función to_json(). Si es así, el valor de la clave '__class__' te dirá cómo decodificar el valor en su tipo de datos original de Python.

  3. Línea 5: Para decodificar la cadena de texto que que devolvió la función time.asctime(), utilizamos la función time.strptime(). Esta función toma una cadena de texto con formato de fecha y hora (en un formato que se puede adaptar, pero que tiene el formato por defecto de la función time.asctime()) y devuelve un objeto time.struct_time.

  4. Línea 7: Para convertir de nuevo la lista de enteros a un objeto bytes puedes utilizar la función bytes().

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

  1. Línea 5: Para utilizar la función from_json() durante el proceso de deserialización, hay que pasarla en el parámetro object_hook a la función json.load(). Una función que toma como parámetro a otra función ¡es muy útil!

  2. Línea 7: La estructura de datos entry ahora contiene una clave 'internal_id' que tiene como valor a un objeto bytes. Y también contiene una clave 'published_date' cuyo valor es un objeto time.struct_time.

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']

  1. Línea 7: Incluso después de utilizar la función to_json() en la serialización y la función from_json() en la deserialización, aún no hemos recreado la réplica perfecta de la estructura original ¿porqué no?

  2. Línea 9: En la estructura de datos original el valor de la clave 'tags' era una tupla de tres cadenas.

  3. Línea 11: Pero en la estructura entry2 el valor de la clave 'tags' es una lista de tres cadenas. JSON no distingue entre tuplas y listas; solamente tiene un tipo de datos parecido a la lista, el array, y el módulo json de Python convierte calladamente ambos tipos, listas y tuplas, en arrays de JSON durante la serialización. Para la mayoría de usos, es posible ignorar esta diferencia, pero conviene saberlo cuando se utiliza este módulo json.

8.12 Lecturas recomendadas

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