2011-08-02 24 views
12

Tengo un problema extraño al usar itertools.groupby para agrupar los elementos de un conjunto de preguntas. Tengo un modelo Resource:itertools.groupby en una plantilla 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 

Tengo un par de recursos en mi base de datos 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' 

Así que si agrupo por tipo, yo, naturalmente, conseguir dos tuplas:

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

Ahora tengo la misma lógica en mi opinión:

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 

Pero cuando iterar sobre esto en mi plantilla, algunos recursos faltan:

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

Esto hace que:

select multiple

Estoy pensando en alguna manera los subiterators se repiten a lo largo ya, pero no estoy seguro de cómo podría pasar esto.

(Usando python 2.7.1, Django 1.3).

(EDIT: Si alguien lee esto, me gustaría recomendar el uso de la incorporada en el regroup template tag en lugar de utilizar groupby.)

Respuesta

16

Creo que tienes razón. No entiendo por qué, pero me parece que su iterador groupby está siendo iterado. Es más fácil de explicar con el código:

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

Hasta ahora, todo bien. Pero, ¿qué ocurre cuando intentamos almacenar los contenidos del iterador en una lista?

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

Seguramente acabamos de almacenar en caché los resultados, y los iteradores siguen siendo buenos. ¿Derecha? Incorrecto.

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

En el proceso de adquisición de las claves, los grupos también se repiten. Así que acabamos de guardar en caché las claves y deshacernos de los grupos, guardar el último elemento.

No sé cómo maneja django los iteradores, pero basado en esto, mi corazonada es que los almacena en caché como listas internamente. Podría al menos confirmar parcialmente esta intuición haciendo lo anterior, pero con más recursos. Si el único recurso que se muestra es el último, entonces es casi seguro que tienes el problema anterior en alguna parte.

+2

Gracias por investigar; Lo intenté con ~ 10 recursos y tenía como máximo un recurso por grupo - lo arreglé poblando el contexto con '(t, list (r)) para t, r en groupby (...)' –

+0

Sí, el el iterador se está iterando previamente, Django convierte el iterador en una lista sin iterar a través de los elementos agrupados. He agregado una explicación en una respuesta separada. –

14

Las plantillas de Django quieren saber la longitud de las cosas que se enrollan utilizando {% for %}, pero los generadores no tienen una longitud.

Entonces Django decide convertirlo a una lista antes de iterar, para que tenga acceso a una lista.

Esto rompe generadores creados usando itertools.groupby. Si no itera a través de cada grupo, pierde los contenidos.Aquí es an example from Django core developer Alex Gaynor, la primera GroupBy normales:

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

aquí es lo que hace Django; que convierte el generador a una lista:

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

Hay dos maneras de evitar esto: convertir a una lista antes de Django Django hace o prevenir de hacerlo.

la conversión en una lista usted mismo

de la imágen:

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

Pero, por supuesto, ya no tienen las ventajas de utilizar un generador, si esto es un problema para usted.

Prevención de Django de la conversión en una lista

La otra forma de evitar esto es lo envuelve en un objeto que proporciona un método __len__ (si se sabe cuál será la longitud):

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

    def __len__(self): 
     return 2 

Django podrá obtener la longitud usando len() y no necesitará convertir su generador en una lista. Es desafortunado que Django haga esto. Tuve la suerte de poder utilizar esta solución, ya que ya estaba usando un objeto así y sabía cuál sería la longitud siempre.

+0

Agradable, contenta alguien con conocimiento Django pesado. – senderle

Cuestiones relacionadas