Лучший способ удаления дубликатов из списка по атрибуту объекта

#python #functional-programming

#python #функциональное программирование

Вопрос:

У меня есть список объектов, и я хочу отфильтровать список таким образом, чтобы в результате было только одно вхождение каждого значения атрибута.

Допустим, у меня есть три объекта

 obj1.my_attr = 'a'
obj2.my_attr = 'b'
obj3.my_attr = 'b'

obj_list = [obj1, obj2, obj3]
  

И, и в конце, я хочу получить [obj1, obj2] . На самом деле порядок не имеет значения, поэтому [obj1, obj3] он точно такой же хороший.

Сначала я подумал о типичных императивных неуклюжих способах, таких как следующие:

 record = set()
result = []

for obj in obj_list:
    if obj.my_attr not in record:
        record.add(obj.my_attr)
        result.append(obj)
  

Затем я подумал о том, чтобы сопоставить его со словарем, использовать ключ для переопределения любой предыдущей записи и, наконец, извлечь значения:

 result = {obj.my_attr: obj for obj in obj_list}.values() 
  

Этот вариант выглядит неплохо, но я хотел бы знать, существует ли какой-либо более элегантный, эффективный или функциональный способ достижения этой цели. Может быть, какая-нибудь приятная штука, скрытая в стандартной библиотеке… Заранее спасибо.

Ответ №1:

Если вы хотите использовать функциональный стиль программирования на Python, вы можете ознакомиться с пакетом toolz. С toolz вы могли бы просто сделать:

 toolz.unique(obj_list, key=lambda x: x.my_attr)
  

Для повышения производительности вы могли бы использовать operator.attrgetter('my_attr') вместо лямбда-функции для ключа. Вы также могли бы использовать cytoolz, который представляет собой быструю реализацию toolz , написанную на Cython.

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

1. Это точно заполняет пробел FP, которого мне не хватало в python. Большое вам спасибо!

Ответ №2:

Вы могли бы использовать объект, который определял бы пользовательскую __hash__ функцию:

 class HashMyAttr:
    def __init__(self, obj):
        self.obj = obj
    def __hash__(self):
        return self.obj.my_attr.__hash__()
    def __eq__(self, other):
         return self.obj.my_attr == other.obj.my_attr
  

И используйте его как:

 obj_list = [x.obj for x in set(HashMyAttr(obj) for obj in obj_list)]
  

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

1. Не совсем. Не работает, если атрибут является int : AttributeError: 'int' object has no attribute '__eq__'

2. в этом случае атрибут выглядит как строка, но я думаю, == тогда это сработало бы.

3. Это определенно работает и является интересным подходом, но я нахожу его немного излишним и довольно подробным. Вместо этого я бы предпочел использовать понимание по словарю (скорее всего, более эффективное, поскольку для этого не нужно создавать оболочки).

4. Если вы можете переопределить __eq__ и __hash__ функции непосредственно в исходном классе ваших объектов, то вам не нужна большая часть накладных расходов здесь.

5. @njzk2 а что, если мне позже понадобится выполнить фильтрацию по другому атрибуту? или у меня уже есть __hash__ реализация?