Python 如何聚合某些字段中具有相同值的多个对象?
我有这个模型:Python 如何聚合某些字段中具有相同值的多个对象?,python,django,Python,Django,我有这个模型: class Connection(models.Model): CONNECTION_CHOICES = [ ('REST-API', 'REST-API'), ('SSH', 'SSH'), ('SFTP', 'SFTP'), ('SOAP-API', 'SOAP-API'), ] endpoint = models.CharField(max_length=240, blank=True,
class Connection(models.Model):
CONNECTION_CHOICES = [
('REST-API', 'REST-API'),
('SSH', 'SSH'),
('SFTP', 'SFTP'),
('SOAP-API', 'SOAP-API'),
]
endpoint = models.CharField(max_length=240, blank=True, null=True)
port = models.IntegerField(blank=True, null=True)
connection_type = models.CharField(max_length=240, choices=CONNECTION_CHOICES)
source_tool = models.ForeignKey(Tool, on_delete=models.CASCADE, related_name='source-tool+')
target_tool = models.ForeignKey(Tool, on_delete=models.CASCADE, related_name='target-tool+')
def __str__(self):
return self.source_tool.name + " to " + self.target_tool.name
def get_absolute_url(self):
return reverse('tools:connection-detail', kwargs={'pk': self.pk})
在视图中,我尝试组合对象,其中源工具和目标工具是相同的,但连接类型不同
目前,我有这样的看法:
def api_map_view(request):
json = {}
nodes = []
links = []
connections = Connection.objects.all()
for connection in connections:
if {'name': connection.source_tool.name, 'id': connection.source_tool.id} not in nodes:
nodes.append({'name': connection.source_tool.name, 'id': connection.source_tool.id})
if {'name': connection.target_tool.name, 'id': connection.target_tool.id} not in nodes:
nodes.append({'name': connection.target_tool.name, 'id': connection.target_tool.id})
if {'source': connection.source_tool.id, 'target': connection.target_tool.id} in links:
links.replace({'source': connection.source_tool.id, 'target': connection.target_tool.id, 'type': links['type'] + '/' + connection_type})
else:
links.append({'source': connection.source_tool.id, 'target': connection.target_tool.id, 'type': connection.connection_type})
json['nodes'] = nodes
json['links'] = links
print(json)
return JsonResponse(data=json)
这个返回,例如
{
'nodes':
[
{'name': 'Ansible', 'id': 1 },
{'name': 'Terraform', 'id': 2},
{'name': 'Foreman', 'id': 3}
],
'links':
[
{'source': 1, 'target': 2, 'type': 'SSH'},
{'source': 2, 'target': 3, 'type': 'REST-API'}
{'source': 1, 'target': 2, 'type': 'REST-API'}
]
}
我的用例是,我想修改连接,我不会为同一个连接获得两个不同的列表条目,只是类型不同。我希望实现以下目标,而不是上面的JSON:
{
'nodes':
[
{'name': 'Ansible', 'id': 1 },
{'name': 'Terraform', 'id': 2},
{'name': 'Foreman', 'id': 3}
],
'links':
[
{'source': 1, 'target': 2, 'type': 'SSH/REST-API'},
{'source': 2, 'target': 3, 'type': 'REST-API'}
]
}
目前,我无法创建查询或修改dict列表来查找条目,其中源和目标与当前条目相同(在列表中迭代),并且无法修改type字段
我将Django3.1与Python3.8一起使用
关于问题发生在以下几行:
if {'source': connection.source_tool.id, 'target': connection.target_tool.id} in links:
links.replace({'source': connection.source_tool.id, 'target': connection.target_tool.id, 'type': links['type'] + '/' + connection_type})
else:
links.append({'source': connection.source_tool.id, 'target': connection.target_tool.id, 'type': connection.connection_type})
您正在检查{'source':X,'target':Y}
是否在链接中,但对于第一次出现的情况,您将{'source':X,'target':Y,'type':Z1}
添加到链接中。因此,对于链接中的,您添加的项目将永远不会是True
,因为它有一个额外的键“type”
另一方面,您不能直接检查链接中的{'source':X,'target':Y,'type':Z1}
,因为当'type':Z2
时,它将不匹配
要解决此问题,请执行以下操作之一:
1.(首选)使用带有键的字典作为源和目标的元组或仅作为源和目标的元组。由于元组和namedtuple是可散列的,因此它们可以用作dict键
import collections # at the top
links = {} # links is now a dict, not a list
SourceTarget = collections.namedtuple('SourceTarget', 'source target')
# >>> SourceTarget('X', 'Y') # to show how they work
# SourceTarget(source='X', target='Y')
要像这样使用:
if (connection.source_tool.id, connection.target_tool.id) in links: # tuples can match with namedtuples
links[SourceTarget(connection.source_tool.id, connection.target_tool.id)] += '/' + connection.connection_type
else:
links[SourceTarget(connection.source_tool.id, connection.target_tool.id)] = connection.connection_type
在您希望它们作为对象/词典列表的末尾:
json['links'] = [{'source': st.source, 'target': st.target, 'type': type_}
for st, type_ in links.items()]
# I used `type_` so that it doesn't conflict with Python own `type()`
2.(1的变体)仍然需要使用元组或命名元组作为dict键,但随后使用with列表来继续追加连接类型
您不需要if/else
部分,只需执行以下操作:
import collections
links = collections.defaultdict(list)
...
# using tuples as the key instead of namedtuples....
links[(connection.source_tool.id, connection.target_tool.id)].append(connection.connection_type)
这将为每个(源、目标)
创建新条目,并将该类型作为列表中的单个值,或者将其附加到该列表中。如果需要检查,则无
并将其转换为json obj:
json['links'] = [{'source': st[0], 'target': st[1], 'type': '/'.join(types)}
for st, types) in links.items()]
顺便说一句,由于您使用的是Python 3.8,因此可以使用“walrus操作符”,以减少重复并使代码更加简洁
以选项1为例,它将使if块的第一部分更加清晰,因为很明显,如果它不存在,您正在添加它;无需阅读整条长线
if (src := {'name': connection.source_tool.name, 'id': connection.source_tool.id}) not in nodes:
nodes.append(src)
if (trg := {'name': connection.target_tool.name, 'id': connection.target_tool.id}) not in nodes:
nodes.append(trg)
if (st := (connection.source_tool.id, connection.target_tool.id)) in links:
# used a tuple to update _existing_ NT element
links[st] += '/' + connection.connection_type
else:
# but must use namedtuples when adding new elements
links[SourceTarget(*st)] = connection.connection_type
这个代码没有意义。links是一个列表,但您在一个地方将其视为dict,并使用一个方法“replace”,而list和dict都没有这个方法。然而,你从来没有遇到过这样的代码,因为你在链接中附加了带有3个键的dict,并测试链接是否有一个带有2个键的dict。这正是我的问题:找到dict的“子部分”(3个值中的2个)来修改第三个值是的,我后来发现这是多次尝试的结果,其中一点链接是一个字符串。FWIW,我更喜欢defaultdict解决方案(当我看到aneroid正在使用它时,我也在制作该解决方案)。我不会将类型加入字符串,但要保留一个列表,这样消费者就可以选择她希望的格式,而不必进行拆分。太棒了!那正是我要找的。非常感谢你!我使用了第一个变体,它就像一个符咒一样工作。我喜欢第二个变体,因为它使用的列表可以将项目分开(不仅仅是使用/
),因此如果您必须进行其他检查,使用项目列表['a','b','c']会更容易
而不是使用字符串a/b/c
。使用元组还是命名元组实际上取决于用例,如果到处使用许多元组,并且会混淆操作的元组类型。如果使用命名元组,请记住将键添加为命名元组;即使键可以“检查”如上所示,针对元组。如果不使用if/then
,则“仅追加”意味着代码更少更清晰。我添加了一个编辑,用于使用Python 3.8赋值表达式可以/应该进行的一些代码清理。使代码更具可读性。