Как получить список объектов с уникальным атрибутом

Фон

У меня есть list. Этот list имеет много объектов. Каждый объект имеет id. Теперь объекты имеют разные типы.

objects = [Aobject, Bobject, Cobject]

где

>>> Aobject != Bobject
True
>>> Aobject.id ==  Bobject.id
True

Проблема

Я хочу list уникальных объектов на основе object.id.

Что-то вроде этого:

set(objects, key=operator.attrgetter('id'))

(Это не работает, но я хочу что-то вроде этого)

Ответ 1

seen = set() 

# never use list as a variable name
[seen.add(obj.id) or obj for obj in mylist if obj.id not in seen]

Это работает, потому что set.add возвращает None, поэтому выражение в понимании списка всегда дает obj, но только если obj.id еще не добавлено в seen.

(Выражение может быть оценено как None, если obj is None; в этом случае obj.id вызовет исключение. Если mylist содержит значения None, измените тест на if obj and (obj.id not in seen))

Обратите внимание, что это даст вам первый объект в списке, который имеет данный идентификатор. @Abhijit ответ даст вам последний такой объект.

Обновление:

В качестве альтернативы, приказ может быть хорошим выбором:

import collections
seen = collections.OrderedDict()

for obj in mylist:
    # eliminate this check if you want the last item
    if obj.id not in seen:
       seen[obj.id] = obj

list(seen.values())

Ответ 2

Как насчет использования dict (поскольку его ключи уникальны)?

Предполагая, что у нас есть

class Object:
    def __init__(self, id):
        self.id = id


Aobject = Object(1)
Bobject = Object(1)
Cobject = Object(2)
objects = [Aobject, Bobject, Cobject]

затем list с уникальным Object полем id можно сгенерировать, используя понимание dict в Python 3

unique_objects = list({object_.id: object_ for object_ in objects}.values())

в Python 2.7

unique_objects = {object_.id: object_ for object_ in objects}.values()

и в Python & lt; 2.7

unique_objects = dict([(object_.id, object_) for object_ in objects]).values()

Наконец, мы можем написать функцию (Python 3 версия)

def unique(elements, key):
    return list({key(element): element for element in elements}.values())

где elements может быть любым iterable, а key - это некоторый callable, который возвращает объекты hashable из elements (key равен operator.attrgetter('id') в нашем конкретном случае случай).

Ответ марцина отлично работает, но не выглядит для меня Pythonic, так как понимание списка изменяет объект seen из внешней области видимости, также есть некоторая магия в использовании метода set.add и сравнении его результата (то есть None) с obj.

И последняя, но не менее важная часть:

Benchmark

setup = '''
import random


class Object:
    def __init__(self, id):
        self.id = id


objects = [Object(random.randint(-100, 100))
           for i in range(1000)]
'''
solution = '''
seen = set()
result = [seen.add(object_.id) or object_
          for object_ in objects
          if object_.id not in seen]
'''
print('list comprehension + set: ',
      min(timeit.Timer(solution, setup).repeat(7, 1000)))
solution = '''
result = list({object_.id: object_
               for object_ in objects}.values())
'''
print('dict comprehension: ',
      min(timeit.Timer(solution, setup).repeat(7, 1000)))

на моей машине выдает

list comprehension + set:  0.20700953400228173
dict comprehension:  0.1477799109998159

Ответ 3

С учетом вашего списка объектов somelist будет что-то вроде

[(Object [A] [1]), (Object [B] [1]), (Object [C] [2]), (Object [D] [2]), (Object [E] [3])]

Вы можете сделать что-то вроде этого

>>> {e.id:e for e in somelist}.values()
[(Object [B] [1]), (Object [D] [2]), (Object [E] [3])]

Ответ 4

Если вы можете изменить класс объектов, вы можете добавить соответствующие методы, которые используются в сравнении сравнения:

# Assumption: this is the 'original' object
class OriginalExampleObject(object):
    def __init__(self, name, nid):
        self.name = name
        self.id = nid
    def __repr__(self):
        return "(OriginalExampleObject [%s] [%s])" % (self.name, self.id)

class SetExampleObj(OriginalExampleObject):
    def __init__(self, name, nid):
        super(SetExampleObj, self).__init__(name, nid)
    def __eq__(self, other):
        return self.id == other.id
    def __hash__(self):
        return self.id.__hash__()


AObject = SetExampleObj("A", 1)
BObject = SetExampleObj("B", 1)
CObject = SetExampleObj("C", 2)

s = set()
s.add(AObject)
s.add(CObject)
print(s)

s.add(BObject)
print(s)

Вывод:

set([(OriginalExampleObject [A] [1]), (OriginalExampleObject [C] [2])])
set([(OriginalExampleObject [A] [1]), (OriginalExampleObject [C] [2])])

Ответ 5

Вы можете просто использовать установочное понимание (наиболее эффективное и простое) так просто, как следует:

unique_list = list({obj for obj in mylist if obj})

Ответ 6

Вы можете использовать рецепт unique_everseen, доступный в itertools документах. Это также доступно в сторонних библиотеках, например toolz.unique. Обратите внимание, что этот метод сохранит первый экземпляр объекта для данного атрибута.

from toolz import unique
from operator import attrgetter

res = list(unique(objects, key=attrgetter('id')))

Если ленивого итератора достаточно, вы можете пропустить преобразование list.

Ответ 7

Достаточно простой способ сделать это:

for obj in mylist:
    if obj.id not in s:
        s.add(obj.id)

И это должно добавить любой идентификатор, который не видел. Затраченное время является линейным по размеру списка источников.

Ответ 8

objects = [Aobject, Bobject, Cobject]
unique_objects = {o['id']:o for o in objects}.values()