Python高效反转列表JSON序列化

4

我正在寻找一种有效的方法来反向序列化一个Python列表。

我尝试使用 json.dumps(reversed(mylist)),但显然 json.dumps 不接受迭代器。

我也可以使用 json.dumps(list(reversed(mylist))),但对于非常大的列表来说效率非常低下,并且我不需要创建临时列表,我希望能够在运行时直接序列化列表而不是创建临时列表。

我认为我可以使用 json.JSONEncoder 来实现这个目的,但我真的不知道应该从 default 函数中返回什么。

由于不能自由安装其他软件包,因此我必须坚持使用标准库。

到目前为止,我尝试了两种方案,以下是测试输出:

>>> timeit.timeit('li.reverse(); json.dumps(li)', number=1, globals=globals())
2.5034537549945526
>>> timeit.timeit('"[{}]".format(",".join(map(json.dumps,reversed(li))))', number=1, globals=globals())
41.076039729989134

我仍然认为实现自己的JSONEncoder会更有效率,但我还不太清楚如何去做。


使用 mylist.reverse() 原地反转列表(避免复制) - 进行序列化,如果需要,则再次反转它? - Jon Clements
这比创建一个新列表要好,但它仍然会创建一个不必要的中间步骤。但还是谢谢你的提示。 :) - silentnights
1
浏览了一下json库,它并不像看起来那么简单。JSONDecoder.default有一个部分说:“例如,为了支持任意迭代器,你可以...”,但这表明你需要从可迭代对象中返回一个列表,这对于子可迭代对象是有意义的(例如,如果你有{test: range(10)}扩展...但不适用于整个数据的reverse。更进一步的复杂性在于,某些层次由C实现处理,而其他部分则由嵌套的_functions处理...为了简单起见,我仍然坚持使用list.reverse :) - Jon Clements
json.dumps(mylist[::-1]) 是另一种实现方式,但会复制列表。 - IceArdor
2个回答

6

避免复制的一种方法是原地反转列表,例如:

mylist.reverse()
json_string = json.dumps(mylist)

如果需要的话,可以通过mylist.reverse()将其翻转回来。


0
在我们疯狂之前,先看看下面的选项是否符合您的性能要求:
mylist.reverse(); json.dumps(mylist); mylist.reverse()
json.dumps(mylist[::-1])
json.dumps(tuple(reversed(mylist)))

你提到了定义自己的JSONEncoder默认函数,这很容易做到(在底部有一个示例*),但我认为它在这里不起作用,因为json.JSONEncoder需要默认函数将对象转换为以下之一:

None, True, False, str, int, float, list, tuple, dict

将迭代器转换为列表或元组会创建一个大对象,而我们正试图避免这种情况。

您要么需要修改您的json库,要么进行猴子补丁。

这是CPython的json.encoder源代码。PyPy、Jython和其他Python实现可能在json模块中使用相同的代码。

https://github.com/python/cpython/blob/master/Lib/json/encoder.py#L204

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,
    dict=dict,
    float=float,
    id=id,
    int=int,
    isinstance=isinstance,
    list=list,
    str=str,
    tuple=tuple,
    _intstr=int.__str__,
    ...
    def _iterencode(o, _current_indent_level):
        if isinstance(o, str):
            yield _encoder(o)
        ...
        elif isinstance(o, (list, tuple)):
            yield from _iterencode_list(o, _current_indent_level)

        # Add support for processing iterators
        elif isinstance(o, iterator_types):
            # Side-effect: this will consume the iterator.
            # This is probably why it's not included in the official json module
            # We could use itertools.tee to be able to iterate over
            # the original iterator while still having an unconsumed iterator
            # but this would require updating all references to the original
            # iterator with the new unconsumed iterator.
            # The side effect may be unavoidable.
            yield from _iterencode_list(o, _current_index_level)

为了提高性能,你会希望在函数外部定义迭代器类型,并将其作为局部变量引入函数中。
str_iterator   = type(iter( str()    ))
list_iterator  = type(iter( list()   ))
tuple_iterator = type(iter( tuple()  ))
range_iterator = type(iter( range(0) ))
list_reverseiterator = type(reversed( list()  )) 
reverseiterator      = type(reversed( tuple() )) #same as <class 'reversed'>

# Add any other iterator classes that you need here, plus any container data types that json doesn't support (sets, frozensets, bytes, bytearray, array.array, numpy.array)
iterator_types = (str_iterator, list_iterator, tuple_iterator, range_iterator,
                  list_reverseiterator, reversed)

如果您想采用猴子补丁的方法,您需要重新定义json.encoder._make_iterencode函数,将所有出现的isinstance(X, (list, tuple))替换为isinstance(X, (list, tuple)+iterator_types)
import json
def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
        _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot,
         iterable_types=_get_iterable_types(),
         ...
    ):
    ...

json.encoder._make_iterencode = _make_iterencode

这些更改看起来像这样:https://github.com/python/cpython/pull/3034/files *如承诺的那样,如何定义自己的默认函数,尽管对于在将迭代器复制到列表或元组之前转储迭代器没有用。
class JSONEncoderThatSupportsIterators(json.JSONEncoder):
    def default(self, o):
        try:
            iterable = iter(o)
        except TypeError:
            pass
        else:
            return list(iterable)
        # Let the base class default method raise the TypeError
        return json.JSONEncoder.default(self, o)

li = range(10000000) # or xrange if Python 2
dumped = JSONEncoderThatSupportsIterators().encode(reversed(li))
assert dumped.startswith('[999999, 999998, 999997, ')
assert dumped.endswith('6, 5, 4, 3, 2, 1, 0]')

或者,你可以定义 default(self, o) 函数,并将其作为参数传递给 json.dumps(default=default),而不是继承 json.JSONEncoder


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接