Как объединить несколько объектов с одинаковыми значениями в определенных полях?

#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, 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})
 

В представлении я пытаюсь объединить объекты, где source_tool и target_tool одинаковы, но connection_type отличается.

В настоящее время у меня есть это представление:

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

Мое использование заключается в том, что я хочу изменить соединения, чтобы я не получал 2 разные записи списка для одного и того же соединения, которые отличаются только типом. Вместо приведенного выше 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'}
           ]
}
 

В настоящее время я не могу создать запрос или изменить список dicts, чтобы найти запись, где источник и цель совпадают с текущей записью (итерация по списку), и изменить поле типа.

Я использую Django 3.1 с Python 3.8.

С уважением

Комментарии:

1. Этот код не имеет смысла. ссылки — это список, но вы обрабатываете его как dict в одном месте с помощью метода «replace», которого нет ни у list, ни у dict. Однако вы никогда не нажимаете на этот код, поскольку вы добавляете dicts с 3 ключами к ссылкам и проверяете, имеет ли links dict с 2 ключами.

2. Это была именно моя проблема: найти «часть» (2 из 3 значений) dict для изменения третьего значения

3. Да, позже я понял, что это были результаты нескольких попыток, когда в какой-то момент ссылки были строкой. FWIW, я предпочитаю решение defaultdict (я также принимал это решение, когда увидел, что aneroid работает над ним). И я бы не стал объединять типы в string, а сохранил бы их в виде списка, чтобы потребитель мог выбирать, как он хочет форматировать, не прибегая к разделению.

Ответ №1:

Проблема возникает в этих строках:

 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} в links , но для первого вхождения вы добавляете {'source': X, 'target': Y, 'type': Z1} в links . Таким образом, добавленный вами элемент никогда не будет True in links использоваться, поскольку у него есть дополнительный ключ ‘type’.

С другой стороны, вы не можете напрямую проверять {'source': X, 'target': Y, 'type': Z1} ссылки in, потому что тогда они не будут совпадать для then case when 'type': Z2 .

Чтобы обойти это, выполните одно из:


1. (предпочтительно) используйте словарь с ключами в качестве namedtuple или просто кортеж источника и цели. Поскольку кортежи и namedtuples являются хешируемыми, их можно использовать в качестве ключей 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) По-прежнему необходимо использовать tuple или namedtuple в качестве ключей dict, но затем использовать defaultdict with list для продолжения добавления типов соединений.

Вам не понадобится 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)
 

Это либо создаст новые записи для каждого (source, target) из них в type виде отдельного значения в списке, либо добавит его в этот список. if Проверка не требуется.

И преобразовать его в ваш json obj:

 json['links'] = [{'source': st[0], 'target': st[1], 'type': '/'.join(types)}
                 for st, types) in links.items()]
 

Кстати, поскольку вы работаете на Python 3.8, вы можете использовать выражения присваивания, известные как «оператор моржа», чтобы уменьшить повторение и сделать код более кратким.

Используя вариант 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
 

Комментарии:

1. Потрясающе! Это было именно то, что я искал. Большое вам спасибо! Я использовал первый вариант, и он работает как шарм.

2. Мне нравится 2-й вариант, потому что он использует списки, в которых элементы хранятся отдельно (и не только с помощью a / , поэтому, если вам нужно было выполнять другие проверки, со списком элементов проще ['a', 'b', 'c'] , чем со строкой a/b/c . И используете ли вы кортежи или именованные кортежи, действительно зависит от варианта использования, и если многие кортежи используются повсюду, и становится непонятно, с каким типом кортежей выполняется операция. Если вы используете namedtuples, не забудьте добавить ключи как namedtuples; хотя ключи могут быть «проверены» на соответствие кортежам, как я показал выше. И «только добавить» без if/then означает меньше кода и понятнее.

3. Я добавил редактирование для некоторой очистки кода, которую вы можете / должны выполнить, используя выражения присваивания Python 3.8. Делает код намного более читаемым.