itertools.groupby в шаблоне django

У меня странная проблема с использованием itertools.groupby для группировки элементов набора запросов. У меня есть модель Resource:

from django.db import models 

TYPE_CHOICES = ( 
    ('event', 'Event Room'),
    ('meet', 'Meeting Room'),
    # etc 
)   

class Resource(models.Model):
    name = models.CharField(max_length=30)
    type = models.CharField(max_length=5, choices=TYPE_CHOICES)
    # other stuff

У меня есть несколько ресурсов в моей базе данных sqlite:

>>> from myapp.models import Resource
>>> r = Resource.objects.all()
>>> len(r)
3
>>> r[0].type
u'event'
>>> r[1].type
u'meet'
>>> r[2].type
u'meet'

Итак, если я группирую по типу, я естественно получаю два кортежа:

>>> from itertools import groupby
>>> g = groupby(r, lambda resource: resource.type)
>>> for type, resources in g:
...   print type
...   for resource in resources:
...     print '\t%s' % resource
event
    resourcex
meet
    resourcey
    resourcez

Теперь у меня та же логика, на мой взгляд:

class DayView(DayArchiveView):
    def get_context_data(self, *args, **kwargs):
        context = super(DayView, self).get_context_data(*args, **kwargs)
        types = dict(TYPE_CHOICES)
        context['resource_list'] = groupby(Resource.objects.all(), lambda r: types[r.type])
        return context

Но когда я перебираю это в своем шаблоне, некоторые ресурсы отсутствуют:

<select multiple="multiple" name="resources">
{% for type, resources in resource_list %}
    <option disabled="disabled">{{ type }}</option>
    {% for resource in resources %}
        <option value="{{ resource.id }}">{{ resource.name }}</option>
    {% endfor %}
{% endfor %}
</select>

Это выглядит как:

выбрать несколько

Я думаю, что субитераторы уже перебираются, но я не уверен, как это могло произойти.

(Используя Python 2.7.1, Django 1.3).

(EDIT: если кто-то читает это, я бы рекомендовал использовать встроенный тег шаблона regroup вместо использования groupby.)


person Ismail Badawi    schedule 02.08.2011    source источник


Ответы (2)


Я думаю, что ты прав. Я не понимаю почему, но мне кажется, что ваш итератор groupby предварительно повторяется. Кодом проще объяснить:

>>> even_odd_key = lambda x: x % 2
>>> evens_odds = sorted(range(10), key=even_odd_key)
>>> evens_odds_grouped = itertools.groupby(evens_odds, key=even_odd_key)
>>> [(k, list(g)) for k, g in evens_odds_grouped]
[(0, [0, 2, 4, 6, 8]), (1, [1, 3, 5, 7, 9])]

Все идет нормально. Но что происходит, когда мы пытаемся сохранить содержимое итератора в виде списка?

>>> evens_odds_grouped = itertools.groupby(evens_odds, key=even_odd_key)
>>> groups = [(k, g) for k, g in evens_odds_grouped]
>>> groups
[(0, <itertools._grouper object at 0x1004d7110>), (1, <itertools._grouper object at 0x1004ccbd0>)]

Конечно, мы только что закешировали результаты, и итераторы по-прежнему хороши. Верно? Неправильный.

>>> [(k, list(g)) for k, g in groups]
[(0, []), (1, [9])]

В процессе получения ключей группы также перебираются. Итак, мы действительно просто кэшировали ключи и выбросили группы, сохранив самый последний элемент.

Я не знаю, как django обрабатывает итераторы, но, основываясь на этом, я предполагаю, что он кэширует их как списки внутри. Вы могли бы хотя бы частично подтвердить эту интуицию, выполнив вышеописанное, но с большим количеством ресурсов. Если единственный отображаемый ресурс является последним, то почти наверняка у вас где-то возникла описанная выше проблема.

person senderle    schedule 02.08.2011
comment
Спасибо за расследование; Я попробовал это с ~ 10 ресурсами и имел не более одного ресурса на группу - я исправил это, заполнив контекст (t, list(r)) for t, r in groupby(...) - person Ismail Badawi; 02.08.2011
comment
Да, итератор проходит предварительную итерацию, Django преобразует итератор в список без повторения сгруппированных элементов. Я добавил объяснение в отдельный ответ. - person Will Hardy; 23.04.2013
comment
это не ответ на вопрос. - person dopatraman; 11.01.2019
comment
@dopatraman Как вы думаете, в чем вопрос? Я думаю, вопрос в том, как-то предварительно итерируются субитераторы? Я так понимаю, вы не согласны? - person senderle; 11.01.2019
comment
Да. Я думаю, вопрос в том, почему произошло это неожиданное событие? Ваш ответ - это подозрение, почему это произошло, а не объяснение. - person dopatraman; 12.01.2019
comment
@dopatraman Даже не подозреваю почему. Это просто подтверждение того, что догадка ОП, вероятно, была верной. (Это является ответом на вопрос, который, как я думаю, был задан. Но мы можем согласиться не согласиться с этим.) - person senderle; 12.01.2019

Шаблоны Django хотят знать длину вещей, которые зациклены с помощью {% for %}, но у генераторов нет длины.

Поэтому Django решает преобразовать его в список перед итерацией, чтобы иметь доступ к списку.

Это ломает генераторы, созданные с использованием itertools.groupby. Если вы не перебираете каждую группу, вы теряете содержимое. Вот пример от основного разработчика Django Алекса Гейнора, сначала обычный groupby:

>>> groups = itertools.groupby(range(10), lambda x: x < 5)
>>> print [list(items) for g, items in groups]
[[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]

Вот что делает Джанго; он преобразует генератор в список:

>>> groups = itertools.groupby(range(10), lambda x: x < 5)
>>> groups = list(groups)
>>> print [list(items) for g, items in groups]
[[], [9]]

Есть два способа обойти это: преобразовать в список до того, как это сделает Django, или запретить Django делать это.

Самостоятельное преобразование в список

Как показано выше:

[(grouper, list(values)) for grouper, values in my_groupby_generator]

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

Предотвращение преобразования Django в список

Обратным способом является обернуть его в объект, который предоставляет метод __len__ (если вы знаете, какой будет длина):

class MyGroupedItems(object):
    def __iter__(self):
        return itertools.groupby(range(10), lambda x: x < 5)

    def __len__(self):
        return 2

Django сможет получить длину, используя len(), и ему не нужно будет преобразовывать ваш генератор в список. К сожалению, Django делает это. Мне повезло, что я мог использовать этот обходной путь, так как я уже использовал такой объект и знал, какой всегда будет длина.

person Will Hardy    schedule 23.04.2013
comment
Хорошо, рад, что кто-то со знанием Django взвесил. - person senderle; 23.04.2013