2011-08-02 16 views
12

Mam dziwny problem przy użyciu itertools.groupby do grupowania elementów zapytania. Mam modelu Resource:itertools.groupby w szablonie django

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 

Mam kilka zasobów w mojej bazy danych 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' 

Więc jeśli grupa ze względu na rodzaj, ja naturalnie dostać dwie krotki:

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

Mam teraz tę samą logikę:

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 

Ale kiedy iteracyjnego w moim szablonu, niektóre zasoby brakuje:

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

Czyni to jako:

select multiple

myślę jakoś subiterators są potwierdzili już ponad, ale nie jestem pewien, jak to się mogło stać.

(Korzystanie z Pythona 2.7.1, Django 1.3).

(EDIT: Jeśli ktoś to czyta, polecam za pomocą wbudowanego w regroup template tag zamiast korzystania groupby).

Odpowiedz

16

Myślę, że masz rację. Nie rozumiem, dlaczego, ale wygląda na to, że iterator jest poddawany wstępnej iteracji. Łatwiej wytłumaczyć za pomocą kodu:

>>> 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])] 

Jak dotąd, tak dobrze. Ale co się stanie, gdy spróbujemy zapisać zawartość iteratora na liście?

>>> 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>)] 

Z pewnością właśnie zbuforowaliśmy wyniki, a iteratory są nadal dobre. Dobrze? Źle.

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

W procesie nabywania kluczy grupy są również iterowane. Więc tak naprawdę właśnie zapisaliśmy w pamięci podręcznej klucze i wyrzuciliśmy grupy, z wyjątkiem ostatniego przedmiotu.

Nie wiem, jak django obsługuje iteratory, ale w oparciu o to, moim przeczuciem jest to, że buforuje je jako listy wewnętrznie. Można przynajmniej częściowo potwierdzić tę intuicję, wykonując powyższe czynności, ale z większą ilością zasobów. Jeśli jedynym wyświetlanym zasobem jest ostatni, to prawie na pewno masz gdzieś powyższy problem.

+2

Dzięki za zbadanie; Próbowałem z ~ 10 zasobami i miałem co najwyżej jeden zasób na grupę - naprawiłem to poprzez zapełnienie kontekstu przez '(t, list (r)) dla t, r w groupby (...)' –

+0

Tak, iterator jest pre-iterowany, Django konwertuje iterator na listę bez iteracji po zgrupowanych elementach. Dodałem wyjaśnienie w osobnej odpowiedzi. –

14

Szablony Django chcą wiedzieć, ile rzeczy jest zapętlonych za pomocą {% for %}, ale generatory nie mają długości.

Django decyduje się przetworzyć go na listę przed iteracją, aby miał dostęp do listy.

To zrywa generatory utworzone przy użyciu itertools.groupby. Jeśli nie wykonasz iteracji w każdej grupie, stracisz zawartość.Oto an example from Django core developer Alex Gaynor, najpierw normalny 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]] 

Oto co robi Django; konwertuje generator do listy:

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

Istnieją dwa sposoby obejścia tego: Konwersja do listy przed Django Django robi lub uniemożliwiają od robią.

Konwersja do listy samemu

Jak pokazano powyżej:

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

Ale oczywiście, nie masz już zalety korzystania z generatora, jeśli jest to problem dla ciebie.

Zapobieganie Django od konwersji do listy

Innym sposobem na to jest owinąć go w obiekt, który dostarcza sposobu __len__ (jeśli wiesz co długość będzie):

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

    def __len__(self): 
     return 2 

Django będzie mógł uzyskać długość za pomocą len() i nie będzie musiał konwertować swojego generatora na listę. To niefortunne, że Django to robi. Miałem szczęście, że mogłem użyć tego rozwiązania, ponieważ już używałem takiego obiektu i wiedziałem, jaka będzie długość.

+0

Miły, zadowolony ktoś z Django wiedzy waży. – senderle

Powiązane problemy