提问人: 提问时间:10/2/2008 最后编辑:5 revsBrian M. Hunt 更新时间:10/7/2008 访问量:3117
Django/Python - 从多对多关系中按公共集对对象进行分组
Django/Python - Grouping objects by common set from a many-to-many relationships
问:
这部分是算法逻辑问题(如何做到),部分是实现问题(如何做到最好!我正在使用 Django,所以我想我会分享它。
在 Python 中,值得一提的是,这个问题在某种程度上与 how-do-i-use-pythons-itertoolsgroupby 有关。
假设你得到了两个 Django 模型派生的类:
from django.db import models
class Car(models.Model):
mods = models.ManyToManyField(Representative)
和
from django.db import models
class Mods(models.Model):
...
如何获得汽车列表,按具有一组通用模组的汽车分组?
即我想上这样的课:
Cars_by_common_mods = [
{ mods: { 'a' }, cars: { 'W1', 'W2' } },
{ mods: { 'a', 'b' }, cars: { 'X1', 'X2', 'X3' }, },
{ mods: { 'b' }, cars: { 'Y1', 'Y2' } },
{ mods: { 'a', 'b', 'c' }, cars: { 'Z1' } },
]
我一直在想这样的事情:
def cars_by_common_mods():
cars = Cars.objects.all()
mod_list = []
for car in cars:
mod_list.append( { 'car': car, 'mods': list(car.mods.all()) }
ret = []
for key, mods_group in groupby(list(mods), lambda x: set(x.mods)):
ret.append(mods_group)
return ret
然而,这是行不通的,因为(也许除其他原因外)groupby 似乎没有按模组集分组。我想必须对mod_list进行排序才能与 groupby 一起使用。总而言之,我相信有一些简单而优雅的东西,既有启发性又有启发性。
干杯和谢谢!
答:
选中重新组合。它仅适用于模板,但我想这种分类无论如何都属于表示层。
评论
您是否尝试过先对列表进行排序?您提出的算法应该有效,尽管有很多数据库命中。
import itertools
cars = [
{'car': 'X2', 'mods': [1,2]},
{'car': 'Y2', 'mods': [2]},
{'car': 'W2', 'mods': [1]},
{'car': 'X1', 'mods': [1,2]},
{'car': 'W1', 'mods': [1]},
{'car': 'Y1', 'mods': [2]},
{'car': 'Z1', 'mods': [1,2,3]},
{'car': 'X3', 'mods': [1,2]},
]
cars.sort(key=lambda car: car['mods'])
cars_by_common_mods = {}
for k, g in itertools.groupby(cars, lambda car: car['mods']):
cars_by_common_mods[frozenset(k)] = [car['car'] for car in g]
print cars_by_common_mods
现在,关于这些查询:
import collections
import itertools
from operator import itemgetter
from django.db import connection
cursor = connection.cursor()
cursor.execute('SELECT car_id, mod_id FROM someapp_car_mod ORDER BY 1, 2')
cars = collections.defaultdict(list)
for row in cursor.fetchall():
cars[row[0]].append(row[1])
# Here's one I prepared earlier, which emulates the sample data we've been working
# with so far, but using the car id instead of the previous string.
cars = {
1: [1,2],
2: [2],
3: [1],
4: [1,2],
5: [1],
6: [2],
7: [1,2,3],
8: [1,2],
}
sorted_cars = sorted(cars.iteritems(), key=itemgetter(1))
cars_by_common_mods = []
for k, g in itertools.groupby(sorted_cars, key=itemgetter(1)):
cars_by_common_mods.append({'mods': k, 'cars': map(itemgetter(0), g)})
print cars_by_common_mods
# Which, for the sample data gives me (reformatted by hand for clarity)
[{'cars': [3, 5], 'mods': [1]},
{'cars': [1, 4, 8], 'mods': [1, 2]},
{'cars': [7], 'mods': [1, 2, 3]},
{'cars': [2, 6], 'mods': [2]}]
现在你已经有了汽车 ID 和 mod id 的列表,如果你需要完整的对象来使用,你可以对每个对象进行一次查询,以获取每个模型的完整列表,并为它们创建一个查找,按它们的 ID 进行键控 - 那么,我相信,Bob 是你众所周知的父亲的兄弟。dict
你在这里有几个问题。
在调用 groupby 之前,您没有对列表进行排序,这是必需的。来自 itertools 文档:
通常,可迭代对象需要已经对同一个键函数进行排序。
然后,您不会复制 groupby 返回的列表。同样,文档指出:
返回的组本身就是一个迭代器,它与 groupby() 中。由于源是共享的,因此当 groupby 对象被推进时, 上一个组不再可见。因此,如果以后需要该数据,它应该 存储为列表:
groups = [] uniquekeys = [] for k, g in groupby(data, keyfunc): groups.append(list(g)) # Store group iterator as a list uniquekeys.append(k)
最后一个错误是使用集合作为键。他们在这里不工作。一个快速的解决方法是将它们转换为排序的元组(可能有更好的解决方案,但我现在想不出来)。
因此,在您的示例中,最后一部分应如下所示:
sortMethod = lambda x: tuple(sorted(set(x.mods)))
sortedMods = sorted(list(mods), key=sortMethod)
for key, mods_group in groupby(sortedMods, sortMethod):
ret.append(list(mods_group))
评论
如果性能是一个问题(即页面上有很多汽车,或者一个高流量的站点),那么非规范化是有意义的,并且可以简化你的问题作为副作用。
请注意,非规范化多对多关系可能有点棘手。我还没有遇到任何这样的代码示例。
感谢大家的有用回复。我一直在解决这个问题。我仍然没有找到“最佳”解决方案,但我有一些想法。
我应该提到我正在使用的数据集的统计数据。在 75% 的情况下,会有一个 Mod。在 24% 的病例中,两个。在 1% 的情况下,将出现零、三个或更多。对于每个 Mod,至少有一辆独特的汽车,尽管一个 Mod 可以应用于许多汽车。
话虽如此,我已经考虑过(但未实现)类似的东西:
class ModSet(models.Model):
mods = models.ManyToManyField(Mod)
并将汽车换成
class Car(models.Model):
modset = models.ForeignKey(ModSet)
按 Car.modset 分组是微不足道的:例如,我可以使用重新分组,正如 Javier 建议的那样。这似乎是一个更简单且相当优雅的解决方案;想法将不胜感激。
评论