В нашем Django-приложении необходимо было разработать отчет (расчет) бонусов.
Отчет должен иметь вложенную структуру с подведением итогов по пользователям, подразделениям и по всей компании. Схематично его логику можно представить:
У этого отчета было два осложняющих момента:
Структура из вложенных словарей отлично решает обе задачи: в них можно сложить все требуемые скаляры (числа, строки, даты), сериализовать и сложить в кэш.
Структура данных для отчета приобрела вид (упрощена):
И вот тут-то я столкнулся с проблемой, что заполнение такой структуры из словарей не столь удобно, как мне того хотелось. Проверка словарей на наличие ключей или использование setdefatult(key, {}) превращает код в нечитабельную кашу.
Эта структура чем-то напоминает XML. И мне бы хотелось использовать что-то подобное тому, как строятся XPath-выражения для адресации узлов XML-дерева:
или на языке Python что-то вида:
Учтывая, что {dept_id} и прочие другие {id} — целые числа, то я разрешил себе использование квадратных скобок: [].
Собственно мне нужен был такой класс, который бы вел себя в основном, как словарь, но при этом:
Так появился ElasticDict
Код по подготовке данных выглядит приблизительно так:
Код в шаблоне так:
Надо отметить, что ElasticDict() это подкласс обычного dict()'а, т.е. в нем доступно все то, что и в обычном словаре. В тот момент, когда потребуется "зафиксировать" структуру (снова захотим получать KeyError'ы при обращении к несуществующим ключам), экземпляр ElasticDict можно экспортировать в обчный dict(). Делается рекурсивный обход ElasticDict(), где все экземпляры этого класа заменяются на обычные словари. Есть и обратное преобразование — на вход подаем словарь, на выходе получаем ElasticDict также с рекурсивным обходом.
Замечания/предложения приветствуются!
UPDATE из англоговорящей тусовки подсказали, что уже есть аналог addict. Думаю, тем, кто проголосовал "мне надо" следует переключиться на него, как на более стабильный (проверенный).
Отчет должен иметь вложенную структуру с подведением итогов по пользователям, подразделениям и по всей компании. Схематично его логику можно представить:
print total
for department in departments:
print department.total
for user in department.users:
print user.total
for row in user.rows:
print row.data
У этого отчета было два осложняющих момента:
- В роли "row" могли выступать разные модели (и располагаться вперемежку), что не позволяет использовать итераторы по QuerySet'ам.
- Время построение отчета. Сбор данных занимает существенное время (несколько секунд). Данные в отчете могут меняться. Говоря на чистоту, это не статический отчет, а инструмент для контроля и корректировки начисленных бонусов в виде отчета. Но данные меняются не очень часто, скажем на каждые 100 просмотров придется одно изменение, после которого нужно перестроить отчет. Т.е. данные можно кэшировать.
Структура из вложенных словарей отлично решает обе задачи: в них можно сложить все требуемые скаляры (числа, строки, даты), сериализовать и сложить в кэш.
Структура данных для отчета приобрела вид (упрощена):
{
'total': {
'income': 1234,
'bonus': 123,
'expense': 1234,
'penalty': 123
},
'departments': {
'{dept_id}': {
'department': {
'title': 'Mega Department'
}
'total': {
'income': 1234,
'bonus': 123,
'expense': 1234,
'penalty': 123
},
'users': {
'{user_id}': {
'user': {
'name': 'John Smith'
},
'total': {
'income': 1234,
'bonus': 123,
'expense': 1234,
'penalty': 123
},
'rows': {
'{sale_id}': { // Одна модель
'type': 'sale'
'base_income': 1234,
'bonus': 123,
'comment': 'some description'
},
'{expense_id}': { // Другая модель !!!
'type': 'expense'
'expense': 1234,
'penalty': 123,
'comment': 'some description'
},
...
}
},
...
}
},
...
}
}
И вот тут-то я столкнулся с проблемой, что заполнение такой структуры из словарей не столь удобно, как мне того хотелось. Проверка словарей на наличие ключей или использование setdefatult(key, {}) превращает код в нечитабельную кашу.
Эта структура чем-то напоминает XML. И мне бы хотелось использовать что-то подобное тому, как строятся XPath-выражения для адресации узлов XML-дерева:
/departments/{dept_id}/users/{user_id}/rows/{row_id}/base_income
или на языке Python что-то вида:
data.departments.{dept_id}.users.{user_id}.rows.{row_id}.base_income
Учтывая, что {dept_id} и прочие другие {id} — целые числа, то я разрешил себе использование квадратных скобок: [].
data.departments[{dept_id}].users[{user_id}].rows[{row_id}].base_income
Собственно мне нужен был такой класс, который бы вел себя в основном, как словарь, но при этом:
- доступ к атрибутам можно было делать без квадратных скобочек
- автоматически создавались отсутствующие аттрибуты
Так появился ElasticDict
В итоге
Код по подготовке данных выглядит приблизительно так:
data = ElasticDict()
for sale in Sale.objects.filter(...).prefetch_related(...):
data.departments[sale.user.department.pk].users[sale.user.pk].rows[sale.pk] = {'base_income': sale.amount, 'bonus': sale.calc_bonus()}
# или в другой форме, кому как больше нравится
for expense in Expense.objects.filter(...).prefetch_related(...):
data.departments[sale.user.department.pk].users[sale.user.pk].rows[expense.pk].base_expense = expense.amount
data.departments[sale.user.department.pk].users[sale.user.pk].rows[expense.pk].penalty = expense.calc_penalty()
Код в шаблоне так:
{{ data.total }}
{% for dept_id, department in data.departments.items %}
{{ department.total }}
{% for user_id, user in department.users.items %}
{{ user.total }}
{% for row_id, row in user.rows.items %}:
{{ row.data }}
{% endfor %}
{% endfor %}
{% endfor %}
Заключение
Надо отметить, что ElasticDict() это подкласс обычного dict()'а, т.е. в нем доступно все то, что и в обычном словаре. В тот момент, когда потребуется "зафиксировать" структуру (снова захотим получать KeyError'ы при обращении к несуществующим ключам), экземпляр ElasticDict можно экспортировать в обчный dict(). Делается рекурсивный обход ElasticDict(), где все экземпляры этого класа заменяются на обычные словари. Есть и обратное преобразование — на вход подаем словарь, на выходе получаем ElasticDict также с рекурсивным обходом.
Замечания/предложения приветствуются!
UPDATE из англоговорящей тусовки подсказали, что уже есть аналог addict. Думаю, тем, кто проголосовал "мне надо" следует переключиться на него, как на более стабильный (проверенный).
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (8)
defuz
28.02.2016 01:54+2Проверка словарей на наличие ключей или использование setdefatult(key, {}) превращает код в нечитабельную кашу.
Окей, давайте сравним. Ваше решение:
from elasticdict import ElasticDict data = ElasticDict() for sale in Sale.objects.filter(...).prefetch_related(...): data.departments[sale.user.department.pk].users[sale.user.pk].rows[sale.pk] = { 'base_income': sale.amount, 'bonus': sale.calc_bonus() }
Решение в лоб, без каких-либо зависимостей, даже без стандартного defaultdict:
data = {'departaments': {}} for sale in Sale.objects.filter(...).prefetch_related(...): departament = data['deparaments'].setdefault(sale.user.department.pk, {'users': {}}) user = departament['users'].setdefault(sale.user.pk, {'rows': {}}) user['rows'][sale.pk] = { 'base_income': sale.amount, 'bonus': sale.calc_bonus() }
Вопрос автору: оно того стоило?Ruzin
28.02.2016 15:51+1Согласен, ради конкретно такого примера не стоило бы.
В моей задаче строчек для вставки в словарь сильно больше (30+), вдобавок, есть внутренняя логика if/else.
Создание промежуточных переменных не добавляет читаемости.
Я не настаиваю, что мой код абсолютно более читаемый, но найдутся те, кто со мной согласятся.
Равно как найдутся те, кто скажет, что это отстой — и последних в 4 раза больше :)
novoxudonoser
Батенька да вы же гигант мысли, да это же просто гениальное изобретение, не знаю, как я мог жить без этого, ведь {}.get('key',None) так не удобен…
Дамс дожили, написал велосипед в ~100 строках — выложил на хабр. Что дальше? Будем стандартную библиотек изобретать?
Ruzin
Этот класс не претендует на вселенское открытие или всепоглощающую сингулярность.
Просто мне показалось, что вот так писать — это не по-питоновски:
Хотелось вот так:
P.S. {}.get('key',None) — не то делает, что мне нужно.
demonMHM
Выглядит, конечно, диковато, но
hellman
Можно рекурсивно:
Ruzin
Спасибо за идеи, мне вот еще одну подкинули в issue:
И дали ссылку на более продвинутую реализацию моего подхода: addict
novoxudonoser
>P.S. {}.get('key',None) — не то делает, что мне нужно.
в статье:
>>1. доступ к атрибутам можно было делать без квадратных скобочек
>> 2. автоматически создавались отсутствующие аттрибуты