19

我在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)。

(编辑:如果有人读到这个,我建议使用内置regroup模板标签而不是使用groupby.)

4

2 回答 2

24

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

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

这会破坏使用itertools.groupby. 如果您不遍历每个组,则会丢失内容。这是Django 核心开发人员 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 这样做了。我很幸运我可以使用这个解决方法,因为我已经在使用这样的对象并且知道长度总是多少。

于 2013-04-23T14:07:26.633 回答
20

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

于 2011-08-02T02:57:22.997 回答