在django模板中的itertools.groupby

Ism*_*awi 16 python django group-by django-templates python-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
Run Code Online (Sandbox Code Playgroud)

我的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'
Run Code Online (Sandbox Code Playgroud)

因此,如果我按类型分组,我自然会得到两个元组:

>>> 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
Run Code Online (Sandbox Code Playgroud)

现在我的观点中有相同的逻辑:

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
Run Code Online (Sandbox Code Playgroud)

但是当我在模板中迭代这个时,会丢失一些资源:

<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>
Run Code Online (Sandbox Code Playgroud)

这呈现为:

选择多个

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

(使用python 2.7.1,Django 1.3).

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

Wil*_*rdy 21

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]]
Run Code Online (Sandbox Code Playgroud)

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

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

有两种方法:在Django之前转换为列表或阻止Django这样做.

自己转换成一个列表

如上图所示:

[(grouper, list(values)) for grouper, values in my_groupby_generator]
Run Code Online (Sandbox Code Playgroud)

但是,当然,如果这对您来说是一个问题,您将不再拥有使用发电机的优势.

阻止Django转换为列表

另一种方法是将它包装在一个提供__len__方法的对象中(如果你知道长度是多少):

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

    def __len__(self):
        return 2
Run Code Online (Sandbox Code Playgroud)

Django将能够使用长度,len()并且不需要将您的生成器转换为列表.不幸的是Django这样做了.我很幸运,我可以使用这种解决方法,因为我已经使用了这样一个对象并且知道长度总是如此.


sen*_*rle 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])]
Run Code Online (Sandbox Code Playgroud)

到现在为止还挺好.但是当我们尝试将迭代器的内容存储在列表中时会发生什么?

>>> 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>)]
Run Code Online (Sandbox Code Playgroud)

当然我们刚刚缓存了结果,迭代器仍然很好.对?错误.

>>> [(k, list(g)) for k, g in groups]
[(0, []), (1, [9])]
Run Code Online (Sandbox Code Playgroud)

在获取密钥的过程中,组也被迭代.所以我们真的只是缓存了密钥并抛弃了组,保存最后一项.

我不知道django如何处理迭代器,但基于此,我的预感是它在内部将它们作为列表进行缓存.你可以通过以上方式至少部分地证实这种直觉,但需要更多的资源.如果显示的唯一资源是最后一个,那么您几乎肯定会遇到上述问题.

  • 谢谢你的调查; 我用~10个资源尝试了它,每个组最多只有一个资源 - 我通过用`(t,list(r))填充上下文来修复它,t,r在groupby(...)中 (2认同)