самый быстрый способ создать JSON для отражения древовидной структуры в Python / Django с помощью mptt



Какой самый быстрый способ в Python (Django) создать JSON на основе набора запросов Django? Обратите внимание, что разбор его в шаблоне, как предложено Здесь, не является вариантом.



Предыстория заключается в том, что я создал метод, который петляет по всем узлам дерева, но уже ужасно медленно преобразует около 300 узлов. Первая (и, вероятно, худшая) идея, которая пришла мне в голову, - это создать json каким-то образом "вручную". Смотрите код ниже.



#! Solution 1 !!#
def quoteStr(input):
return """ + smart_str(smart_unicode(input)) + """

def createJSONTreeDump(user, node, root=False, lastChild=False):
q = """

#open tag for object
json = str("n" + indent + "{" +
quoteStr("name") + ": " + quoteStr(node.name) + ",n" +
quoteStr("id") + ": " + quoteStr(node.pk) + ",n" +
)

childrenTag = "children"
children = node.get_children()
if children.count() > 0 :
#create children array opening tag
json += str(indent + quoteStr(childrenTag) + ": [")
#for child in children:
for idx, child in enumerate(children):
if (idx + 1) == children.count():
//recursive call
json += createJSONTreeDump(user, child, False, True, layout)
else:
//recursive call
json += createJSONTreeDump(user, child, False, False, layout)
#add children closing tag
json += "]n"

#closing tag for object
if lastChild == False:
#more children following, add ","
json += indent + "},n"
else:
#last child, do not add ","
json += indent + "}n"
return json


Дерево структура, подлежащая визуализации, представляет собой дерево, построенное с помощью mptt, где вызов .get_children() возвращает все дочерние элементы узла.



Модель выглядит так же просто, как и эта, mptt заботится обо всем остальном.



class Node(MPTTModel, ExtraManager):
"""
Representation of a single node
"""
name = models.CharField(max_length=200)
parent = TreeForeignKey('self', null=True, blank=True, related_name='%(app_label)s_%(class)s_children')


Ожидаемый результат JSON , созданный таким образом в шаблоне var root = {{ jsonTree|safe }}



Править:
На основе этого ответа я создал следующий код (определенно лучший код), но чувствует себя только немного быстрее.



Решение 2:



def serializable_object(node):
"Recurse into tree to build a serializable object"
obj = {'name': node.name, 'id': node.pk, 'children': []}
for child in node.get_children():
obj['children'].append(serializable_object(child))
return obj

import json
jsonTree = json.dumps(serializable_object(nodeInstance))


Решение 3:



def serializable_object_List_Comprehension(node):
"Recurse into tree to build a serializable object"
obj = {
'name': node.name,
'id': node.pk,
'children': [serializable_object(ch) for ch in node.get_children()]
}
return obj


Решение 4:



def recursive_node_to_dict(node):
result = {
'name': node.name, 'id': node.pk
}
children = [recursive_node_to_dict(c) for c in node.get_children()],
if children is not None:
result['children'] = children
return result

from mptt.templatetags.mptt_tags import cache_tree_children
root_nodes = cache_tree_children(root.get_descendants())
dicts = []
for n in root_nodes:
dicts.append(recursive_node_to_dict(root_nodes[0]))
jsonTree = json.dumps(dicts, indent=4)


Решение 5 (Используйте select_related для pre_fetch, тогда как не уверен, правильно ли используется)



def serializable_object_select_related(node):
"Recurse into tree to build a serializable object, make use of select_related"
obj = {'name': node.get_wbs_code(), 'wbsCode': node.get_wbs_code(), 'id': node.pk, 'level': node.level, 'position': node.position, 'children': []}
for child in node.get_children().select_related():
obj['children'].append(serializable_object(child))
return obj


Решение 6 (улучшенное решение 4, использующее кэширование дочерних узлов):



def recursive_node_to_dict(node):
result = {
'name': node.name, ''id': node.pk,
#notice the use of node._cached_children instead of node.get_children()
'children' : [recursive_node_to_dict(c) for c in node._cached_children]
}


Вызывается через:



from mptt.templatetags.mptt_tags import cache_tree_children
subTrees = cache_tree_children(root.get_descendants(include_self=True))
subTreeDicts = []
for subTree in subTrees:
subTree = recursive_node_to_dict(subTree)
subTreeDicts.append(subTree)
jsonTree = json.dumps(subTreeDicts, indent=4)
#optional clean up, remove the [ ] at the beginning and the end, its needed for D3.js
jsonTree = jsonTree[1:len(jsonTree)]
jsonTree = jsonTree[:len(jsonTree)-1]


Ниже вы можете увидеть результаты профилирования, созданные с помощью cProfile, как предлагает MuMind, настраивая представление Django для запуска автономного метода profileJSON (), который в свою очередь вызывает различные решения для создания вывода JSON.



def startProfileJSON(request):
print "startProfileJSON"
import cProfile
cProfile.runctx('profileJSON()', globals=globals(), locals=locals())
print "endProfileJSON"


Результаты:



Решение 1: 3350347 вызовов функций (3130372 примитивных вызова) за 4,969 секунды (подробности )



Решение 2: 2533705 вызовов функций (2354516 примитивных вызовов) за 3,630 секунды (подробности )



Решение 3: 2533621 вызовов функций (2354441 примитивных вызовов) за 3,684 секунды (подробности )



Решение 4: 2812725 вызовы функций (2466028 примитивных вызовов) за 3,840 секунды (подробности)



Решение 5: 2536504 вызова функций (2357256 примитивных вызовов) за 3,779 секунды (подробности )



Решение 6 (улучшенное решение 4): 2593122 вызова функций (2299165 примитивных вызовов) за 3,663 секунды (подробности )



Обсуждение:



Решение 1: собственная реализация кодирования. плохая идея



Решение 2 + 3: в настоящее время Самый быстрый , но все равно мучительно медленный



Решение 4: выглядит многообещающим с кэшированием childs, но выполняет аналогичные действия и в настоящее время производит недопустимый json, поскольку дети помещаются в double []:



"children": [[]] instead of "children": []


Решение 5: Использование select_related не имеет значения, в то время как, вероятно, используется неверно, так как узел всегда имеет иностранный ключ к своему родителю, и мы анализируем от корня к дочернему.



Обновление: решение 6: по-моему, это самое чистое решение, использование кэширования дочерних узлов. Но выполняет только аналогично решению 2 + 3. Что для меня странно.



Есть еще идеи по улучшению производительности?

808   4  

4 ответов:

Я подозреваю, что самое большое замедление заключается в том, что это будет делать 1 запрос базы данных на узел. Рендеринг json тривиален по сравнению с сотнями обращений к вашей базе данных.

Вы должны кэшировать дочерние элементы на каждом узле, чтобы эти запросы можно было выполнять все сразу. django-mptt имеет функцию cache_tree_children(), с помощью которой это можно сделать.

import json
from mptt.templatetags.mptt_tags import cache_tree_children

def recursive_node_to_dict(node):
    result = {
        'id': node.pk,
        'name': node.name,
    }
    children = [recursive_node_to_dict(c) for c in node.get_children()]
    if children:
        result['children'] = children
    return result

root_nodes = cache_tree_children(Node.objects.all())
dicts = []
for n in root_nodes:
    dicts.append(recursive_node_to_dict(n))

print json.dumps(dicts, indent=4)

Пользовательское кодирование json, хотя оно может обеспечить небольшое ускорение в некоторых сценариях, - это то, что я бы очень хотел обескуражьте, так как это будет много кода, и это то, что легко получить очень неправильно.

Ваша обновленная версия выглядит так, что накладных расходов будет очень мало. Я думаю, что это было бы немного более эффективно (и более читабельно!) использовать понимание списка:

def serializable_object(node):
    "Recurse into tree to build a serializable object"
    obj = {
        'name': node.name,
        'children': [serializable_object(ch) for ch in node.get_children()]
    }
    return obj
Кроме того, все, что вы можете сделать, это профилировать его, чтобы найти узкие места. Напишите какой-нибудь автономный код, который загружает и сериализует ваши 300 узлов, а затем запустите его с помощью
python -m profile serialize_benchmark.py

(или -m cProfile, Если это работает лучше).

Можно увидеть 3 различных потенциальных узких места:

  • доступ к БД (.get_children() и .name) - я точно не знаю, что происходит под капотом, но у меня был такой код, который делает запрос БД для каждого узла, добавляя огромные накладные расходы. Если это ваша проблема, вы, вероятно, можете настроить это, чтобы сделать "нетерпеливую нагрузку", используя select_related или что-то подобное.
  • накладные расходы на вызов функции (например, сам serializable_object) - Просто убедитесь, что ncalls для serializable_object выглядит как разумное число. Если я правильно понял ваше описание, это должно быть в районе 300.
  • сериализация в конце (json.dumps(nodeInstance)) - не вероятный виновник, так как вы сказали, что это только 300 узлов, но если вы видите, что это занимает много времени выполнения, убедитесь, что у вас есть скомпилированные ускорения для JSON, работающие должным образом.

Если вы не можете сказать много от профилирования, сделайте урезанную версию, которая, скажем, рекурсивно вызывает node.name и node.get_children(), но не сохраняет результаты в структуре данных, и посмотрите, как это сопоставляется.


Обновление: Всего 2192 звонка к execute_sql в решении 3 и 2192 в решении 5, поэтому я думаю, что избыточные запросы БД-это проблема, и что select_related ничего не сделал так, как это было использовано выше. Глядя на django-mptt issue #88: Allow select_related in model methods предполагает, что вы используете его более или менее правильно, но у меня есть сомнения, и get_children против get_descendants может иметь огромное значение.

Есть также тонна времени, занятого copy.deepcopy, что озадачивает, потому что вы не вызываете его напрямую, и я не вижу он звонил из MPT-кода. Что такое tree.py?

Если вы много работаете с профилированием, я бы настоятельно рекомендовал действительно скользкий инструмент RunSnakeRun, который позволяет вам видеть данные вашего профиля в действительно удобной форме сетки и быстрее понимать смысл данных.

В любом случае, вот еще одна попытка упорядочить DB-сторону вещей:

import weakref
obj_cache = weakref.WeakValueDictionary()

def serializable_object(node):
    root_obj = {'name': node.get_wbs_code(), 'wbsCode': node.get_wbs_code(),
            'id': node.pk, 'level': node.level, 'position': node.position,
            'children': []}
    obj_cache[node.pk] = root_obj
    # don't know if the following .select_related() does anything...
    for descendant in node.get_descendants().select_related():
        # get_descendants supposedly traverses in "tree order", which I think
        # means the parent obj will always be created already
        parent_obj = obj_cache[descendant.parent.pk]    # hope parent is cached
        descendant_obj = {'name': descendant.get_wbs_code(),
            'wbsCode': descendant.get_wbs_code(), 'id': descendant.pk,
            'level': descendant.level, 'position': descendant.position,
            'children': []}
        parent_obj['children'].append(descendant_obj)
        obj_cache[descendant.pk] = descendant_obj
    return root_obj

Обратите внимание, что это больше не рекурсивно. Он проходит итеративно через узлы, теоретически после их родителей были посещены, и все это использует один большой вызов MPTTModel.get_descendants(), так что, надеюсь, что это хорошо оптимизировано и кэширует .parent и т. д. (или, может быть, есть более прямой способ добраться до родительского ключа?). Он создает каждый объект без детей изначально, а затем "прививает" все ценности их родителям впоследствии.

Организуйте данные во вложенные словари или списки, а затем вызовите метод JSON dump:

import json   
data = ['foo', {'bar': ('baz', None, 1.0, 2)}]
json.dump(data)

Немного поиграв с этим, я обнаружил, что все решения были слишком медленными, потому что mptt сам сканировал кэш несколько раз до get_children.

Используя тот факт, что mptt возвращает строки в правильном порядке, чтобы легко строить деревья, я сделал следующее:

def flat_tree_to_dict(nodes, max_depth):
    tree = []
    last_levels = [None] * max_depth
    for n in nodes:
        d = {'name': n.name}
        if n.level == 0:
            tree.append(d)
        else:
            parent_dict = last_levels[n.level - 1]
            if 'children' not in parent_dict:
                parent_dict['children'] = []
            parent_dict['children'].append(d)
        last_levels[n.level] = d
    return tree

Для моего набора данных это работает в 10 раз быстрее, чем другие решения, потому что это O (n), только повторяя данные один раз.

Я использую его так:

json.dumps(flat_tree_to_dict(Model.objects.all(), 4), indent=4)

Comments

    Ничего не найдено.