Django / Python-通过多对多关系按通用集对对象进行分组

时间:2020-03-06 15:00:15  来源:igfitidea点击:

这部分是算法逻辑问题(如何执行),部分是实现问题(如何做到最好!)。我正在使用Django,因此我想与大家分享。

在Python中,值得一提的是,该问题与how-do-i-use-pythons-itertoolsgroupby有关。

假设我们给了两个Django Model派生的类:

from django.db import models

class Car(models.Model):
    mods = models.ManyToManyField(Representative)

from django.db import models

class Mods(models.Model):
   ...

如何获得按汽车分类并具有一组常见Mod的汽车列表?

IE。我想上这样的课:

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似乎没有按照mods集进行分组。我猜想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的列表,如果我们需要使用完整的对象,则可以对每个对象进行一次查询,以获取每个模型的完整列表,并为这些模型创建查找" dict" ,以他们的身分证明为依据,我相信,鲍勃(Bob)是你那名父亲的兄弟。

我们在这里遇到一些问题。

在调用groupby之前,我们没有对列表进行排序,这是必需的。从itertools文档中:

Generally, the iterable needs to already be sorted on the same key function.

然后,我们不会复制groupby返回的列表。同样,文档指出:

The returned group is itself an iterator that shares the underlying iterable with
  groupby(). Because the source is shared, when the groupby object is advanced, the
  previous group is no longer visible. So, if that data is needed later, it should 
  be stored as a list:

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,至少有一个唯一的Car,尽管Mod可以应用于许多Car。

话虽如此,我已经考虑(但未实现)类似的东西:

class ModSet(models.Model):
  mods = models.ManyToManyField(Mod)

然后把车换成

class Car(models.Model):
  modset = models.ForeignKey(ModSet)

按Car.modset分组很简单:例如,我可以使用regroup,例如Javier的建议。这似乎是一个更简单且合理的解决方案。的想法将不胜感激。