2012-10-01 11 views
10

Estoy escribiendo un servicio web que devuelve objetos que contienen listas muy largas, codificadas en JSON. Por supuesto, queremos utilizar iteradores en lugar de listas de Python para que podamos transmitir los objetos desde una base de datos; desafortunadamente, el codificador JSON en la biblioteca estándar (json.JSONEncoder) solo acepta listas y tuplas para convertirlas a listas JSON (aunque _iterencode_list parece que realmente funcionaría en cualquier iterable).Ison-encoding iterators muy largos

Las cadenas de documentación sugieren defecto primordial para convertir el objeto a una lista, pero esto significa que perdemos los beneficios de la transmisión. Anteriormente, reemplazamos un método privado, pero (como era de esperar) que se rompió cuando se refactorizó el codificador.

¿Cuál es la mejor manera de serializar iteradores como listas de JSON en Python de una forma de streaming?

Respuesta

3

Necesitaba exactamente esto. El primer enfoque fue anulado el método JSONEncoder.iterencode(). Sin embargo, esto no funciona porque, tan pronto como el iterador no está a nivel, las funciones internas de alguna función _iterencode() se hacen cargo.

Después de estudiar el código, encontré una solución muy hacky, pero funciona.Python 3 solo, pero estoy seguro que la misma magia es posible con Python 2 (solo otros nombres método mágico):

import collections.abc 
import json 
import itertools 
import sys 
import resource 
import time 
starttime = time.time() 
lasttime = None 


def log_memory(): 
    if "linux" in sys.platform.lower(): 
     to_MB = 1024 
    else: 
     to_MB = 1024 * 1024 
    print("Memory: %.1f MB, time since start: %.1f sec%s" % (
     resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/to_MB, 
     time.time() - starttime, 
     "; since last call: %.1f sec" % (time.time() - lasttime) if lasttime 
     else "", 
    )) 
    globals()["lasttime"] = time.time() 


class IterEncoder(json.JSONEncoder): 
    """ 
    JSON Encoder that encodes iterators as well. 
    Write directly to file to use minimal memory 
    """ 
    class FakeListIterator(list): 
     def __init__(self, iterable): 
      self.iterable = iter(iterable) 
      try: 
       self.firstitem = next(self.iterable) 
       self.truthy = True 
      except StopIteration: 
       self.truthy = False 

     def __iter__(self): 
      if not self.truthy: 
       return iter([]) 
      return itertools.chain([self.firstitem], self.iterable) 

     def __len__(self): 
      raise NotImplementedError("Fakelist has no length") 

     def __getitem__(self, i): 
      raise NotImplementedError("Fakelist has no getitem") 

     def __setitem__(self, i): 
      raise NotImplementedError("Fakelist has no setitem") 

     def __bool__(self): 
      return self.truthy 

    def default(self, o): 
     if isinstance(o, collections.abc.Iterable): 
      return type(self).FakeListIterator(o) 
     return super().default(o) 

print(json.dumps((i for i in range(10)), cls=IterEncoder)) 
print(json.dumps((i for i in range(0)), cls=IterEncoder)) 
print(json.dumps({"a": (i for i in range(10))}, cls=IterEncoder)) 
print(json.dumps({"a": (i for i in range(0))}, cls=IterEncoder)) 


log_memory() 
print("dumping 10M numbers as incrementally") 
with open("/dev/null", "wt") as fp: 
    json.dump(range(10000000), fp, cls=IterEncoder) 
log_memory() 
print("dumping 10M numbers built in encoder") 
with open("/dev/null", "wt") as fp: 
    json.dump(list(range(10000000)), fp) 
log_memory() 

Resultados:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 
[] 
{"a": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]} 
{"a": []} 
Memory: 8.4 MB, time since start: 0.0 sec 
dumping 10M numbers as incrementally 
Memory: 9.0 MB, time since start: 8.6 sec; since last call: 8.6 sec 
dumping 10M numbers built in encoder 
Memory: 395.5 MB, time since start: 17.1 sec; since last call: 8.5 sec 

Es claro ver que la IterEncoder hace no necesita la memoria para almacenar 10M de entrada, manteniendo la misma velocidad de codificación.

El truco (hacky) es que el _iterencode_list en realidad no necesita ninguna de las cosas de la lista. Solo quiere saber si la lista está vacía (__bool__) y luego obtener su iterador. Sin embargo, solo llega a este código cuando isinstance(x, (list, tuple)) devuelve True. Así que estoy empaquetando el iterador en una subclase de lista, deshabilitando todo el acceso aleatorio, obteniendo el primer elemento por adelantado para saber si está vacío o no, y devolviendo el iterador. Entonces, el método default devuelve esta lista falsa en el caso de un iterador.

+1

+1 Es una excelente solución que me inspiró a escribir una variante más corta similar de FakeListIterator en mi [respuesta a una pregunta similar] (https://stackoverflow.com/a/46841935/448474). No es necesario que se generen excepciones y todo funciona sin problemas, incluidos '__len__',' __bool__', '__repr__', etc., que no se anulan. – hynekcer

+0

Por cierto, si solo reemplaza el método 'predeterminado' de' JSONEncoder' no necesita subclasificarlo, simplemente defina una función y páselo al 'predeterminado' kwarg de' json.dumps (obj, default = .. .) ' – cowbert

-1

No es tan simple. El protocolo WSGI (que es lo que la mayoría de la gente usa) no es compatible con la transmisión. Y los servidores que sí lo admiten están violando las especificaciones.

E incluso si se utiliza un servidor que no es compatible, entonces usted tiene que usar algo como ijson. también echar un vistazo a este tipo que tenía el mismo problema que usted http://www.enricozini.org/2011/tips/python-stream-json/

EDIT: (?) Luego todo se reduce al cliente, que supongo que será escrito en Javascript. Pero no veo cómo se pueden construir objetos javascript (o cualquier otro idioma) a partir de incompletos JSON chuncks. Lo único que se me ocurre es dividir manualmente el JSON largo en objetos JSON más pequeños (en el lado del servidor) y luego transmitirlo, uno por uno, al cliente. Pero esto requiere websockets y no solicitudes/respuestas http sin estado. Y si por servicio web te refieres a una API REST, entonces supongo que no es lo que quieres.

+1

Mi pregunta realmente no tiene nada que ver con WSGI. ijson es un analizador, no un codificador. – Max

+0

Nada en PEP 3333 (el protocolo WSGI) prohíbe las respuestas de transmisión. La única estipulación es que el servidor WSGI no debe almacenarse en el búfer ni bloquear internamente una vez que comienza a escribir en el cliente. La mayoría de los servidores WSGI invocan continuamente fflush (3) o abstracción comparable en el socket de salida al recibir un bytest de la aplicación, por lo que es perfectamente aceptable iterar sobre un generador (el objeto cedido se envía inmediatamente al socket después de la serialización). – cowbert

0

de streaming real no está bien apoyado por json, ya que también significaría que la aplicación cliente también tendrá que soportar streaming. Hay algunas bibliotecas de Java que admiten la lectura de flujos transmitidos por streaming json, pero no es muy genérico. También hay algunos enlaces de python para yail, que es una biblioteca C que admite transmisión.

Tal vez se puede utilizar en lugar de Yamljson. Yaml es un superconjunto de json. Tiene mejor soporte para la transmisión en ambos lados y cualquier mensaje json seguirá siendo válido yaml.

Pero en su caso, puede ser más simple tanto para dividir el flujo de objeto en un flujo de mensajes separados json.

Ver también esta discusión aquí, que las bibliotecas de cliente de streaming apoyan: Is there a streaming API for JSON?

+3

La pregunta * ¿Existe una API de transmisión para JSON? * Se trata de analizar JSON sin crearlo. –

+0

Esto simplemente no es cierto: "también significaría que la aplicación del cliente deberá soportar la transmisión". En el lado del servidor, puede que le importe la huella de memoria, mientras que en el cliente puede mantener el objeto completo en la memoria .. –

+0

¿Cómo transmitiría una lista parcial de un servidor a un cliente que no admite la transmisión? –

2

Guardar esto en un archivo de módulo y la importación o pegarlo directamente en el código.

''' 
Copied from Python 2.7.8 json.encoder lib, diff follows: 
@@ -331,6 +331,8 @@ 
        chunks = _iterencode(value, _current_indent_level) 
       for chunk in chunks: 
        yield chunk 
+  if first: 
+   yield buf 
     if newline_indent is not None: 
      _current_indent_level -= 1 
      yield '\n' + (' ' * (_indent * _current_indent_level)) 
@@ -427,12 +429,12 @@ 
      yield str(o) 
     elif isinstance(o, float): 
      yield _floatstr(o) 
-  elif isinstance(o, (list, tuple)): 
-   for chunk in _iterencode_list(o, _current_indent_level): 
-    yield chunk 
     elif isinstance(o, dict): 
      for chunk in _iterencode_dict(o, _current_indent_level): 
       yield chunk 
+  elif hasattr(o, '__iter__'): 
+   for chunk in _iterencode_list(o, _current_indent_level): 
+    yield chunk 
     else: 
      if markers is not None: 
       markerid = id(o) 
''' 
from json import encoder 

def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, 
     _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot, 
     ## HACK: hand-optimized bytecode; turn globals into locals 
     ValueError=ValueError, 
     basestring=basestring, 
     dict=dict, 
     float=float, 
     id=id, 
     int=int, 
     isinstance=isinstance, 
     list=list, 
     long=long, 
     str=str, 
     tuple=tuple, 
    ): 

    def _iterencode_list(lst, _current_indent_level): 
     if not lst: 
      yield '[]' 
      return 
     if markers is not None: 
      markerid = id(lst) 
      if markerid in markers: 
       raise ValueError("Circular reference detected") 
      markers[markerid] = lst 
     buf = '[' 
     if _indent is not None: 
      _current_indent_level += 1 
      newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) 
      separator = _item_separator + newline_indent 
      buf += newline_indent 
     else: 
      newline_indent = None 
      separator = _item_separator 
     first = True 
     for value in lst: 
      if first: 
       first = False 
      else: 
       buf = separator 
      if isinstance(value, basestring): 
       yield buf + _encoder(value) 
      elif value is None: 
       yield buf + 'null' 
      elif value is True: 
       yield buf + 'true' 
      elif value is False: 
       yield buf + 'false' 
      elif isinstance(value, (int, long)): 
       yield buf + str(value) 
      elif isinstance(value, float): 
       yield buf + _floatstr(value) 
      else: 
       yield buf 
       if isinstance(value, (list, tuple)): 
        chunks = _iterencode_list(value, _current_indent_level) 
       elif isinstance(value, dict): 
        chunks = _iterencode_dict(value, _current_indent_level) 
       else: 
        chunks = _iterencode(value, _current_indent_level) 
       for chunk in chunks: 
        yield chunk 
     if first: 
      yield buf 
     if newline_indent is not None: 
      _current_indent_level -= 1 
      yield '\n' + (' ' * (_indent * _current_indent_level)) 
     yield ']' 
     if markers is not None: 
      del markers[markerid] 

    def _iterencode_dict(dct, _current_indent_level): 
     if not dct: 
      yield '{}' 
      return 
     if markers is not None: 
      markerid = id(dct) 
      if markerid in markers: 
       raise ValueError("Circular reference detected") 
      markers[markerid] = dct 
     yield '{' 
     if _indent is not None: 
      _current_indent_level += 1 
      newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) 
      item_separator = _item_separator + newline_indent 
      yield newline_indent 
     else: 
      newline_indent = None 
      item_separator = _item_separator 
     first = True 
     if _sort_keys: 
      items = sorted(dct.items(), key=lambda kv: kv[0]) 
     else: 
      items = dct.iteritems() 
     for key, value in items: 
      if isinstance(key, basestring): 
       pass 
      # JavaScript is weakly typed for these, so it makes sense to 
      # also allow them. Many encoders seem to do something like this. 
      elif isinstance(key, float): 
       key = _floatstr(key) 
      elif key is True: 
       key = 'true' 
      elif key is False: 
       key = 'false' 
      elif key is None: 
       key = 'null' 
      elif isinstance(key, (int, long)): 
       key = str(key) 
      elif _skipkeys: 
       continue 
      else: 
       raise TypeError("key " + repr(key) + " is not a string") 
      if first: 
       first = False 
      else: 
       yield item_separator 
      yield _encoder(key) 
      yield _key_separator 
      if isinstance(value, basestring): 
       yield _encoder(value) 
      elif value is None: 
       yield 'null' 
      elif value is True: 
       yield 'true' 
      elif value is False: 
       yield 'false' 
      elif isinstance(value, (int, long)): 
       yield str(value) 
      elif isinstance(value, float): 
       yield _floatstr(value) 
      else: 
       if isinstance(value, (list, tuple)): 
        chunks = _iterencode_list(value, _current_indent_level) 
       elif isinstance(value, dict): 
        chunks = _iterencode_dict(value, _current_indent_level) 
       else: 
        chunks = _iterencode(value, _current_indent_level) 
       for chunk in chunks: 
        yield chunk 
     if newline_indent is not None: 
      _current_indent_level -= 1 
      yield '\n' + (' ' * (_indent * _current_indent_level)) 
     yield '}' 
     if markers is not None: 
      del markers[markerid] 

    def _iterencode(o, _current_indent_level): 
     if isinstance(o, basestring): 
      yield _encoder(o) 
     elif o is None: 
      yield 'null' 
     elif o is True: 
      yield 'true' 
     elif o is False: 
      yield 'false' 
     elif isinstance(o, (int, long)): 
      yield str(o) 
     elif isinstance(o, float): 
      yield _floatstr(o) 
     elif isinstance(o, dict): 
      for chunk in _iterencode_dict(o, _current_indent_level): 
       yield chunk 
     elif hasattr(o, '__iter__'): 
      for chunk in _iterencode_list(o, _current_indent_level): 
       yield chunk 
     else: 
      if markers is not None: 
       markerid = id(o) 
       if markerid in markers: 
        raise ValueError("Circular reference detected") 
       markers[markerid] = o 
      o = _default(o) 
      for chunk in _iterencode(o, _current_indent_level): 
       yield chunk 
      if markers is not None: 
       del markers[markerid] 

    return _iterencode 

encoder._make_iterencode = _make_iterencode 
+0

http://bugs.python.org/issue14573 – Zectbumo