在django模板中的itertools.groupby

时间:2011-08-02 02:10:40

标签: python django group-by django-templates itertools

使用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>

这呈现为:

select multiple

我在想某些subiterator已经被迭代了,但我不确定这是怎么发生的。

(使用python 2.7.1,Django 1.3)。

(编辑:如果有人读到此内容,我建议您使用内置regroup template tag代替groupby。)

2 个答案:

答案 0 :(得分:21)

Django的模板想知道使用{% for %}循环的事物的长度,但生成器没有长度。

因此Django决定在迭代之前将其转换为列表,以便它可以访问列表。

这会破坏使用itertools.groupby创建的生成器。如果不遍历每个组,则会丢失内容。这是an example from Django core developer Alex Gaynor,首先是普通的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]]

这是Django的作用;它将生成器转换为列表:

>>> 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这样做了。我很幸运,我可以使用这种解决方法,因为我已经使用了这样一个对象并且知道长度总是如此。

答案 1 :(得分:18)

我认为你是对的。我不明白为什么,但它看起来像你的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如何处理迭代器,但基于此,我的预感是它在内部将它们作为列表进行缓存。你可以通过以上方式至少部分地证实这种直觉,但需要更多的资源。如果显示的唯一资源是最后一个,那么您几乎肯定会遇到上述问题。