Pythonic способ игнорирования последнего элемента при выполнении разницы между наборами

11

Допустим, у меня есть два set()с:

a = {('1', '2', '3', 'a'), ('1', '2', '4', 'a'), ('1', '2', '5', 'b')}
b = {('1', '2', '3', 'b'), ('1', '2', '4', 'b'), ('1', '2', '6', 'b')}

Теперь я хочу найти разницу в множестве, b \ aно проигнорировать последний элемент из каждого кортежа. Так что это как делать что-то вроде этого:

a = {('1', '2', '3'), ('1', '2', '4'), ('1', '2', '5')}
b = {('1', '2', '3'), ('1', '2', '4'), ('1', '2', '6')}

In[1]: b - a
Out[1]: {('1', '2', '6')}

Ожидаемый результат:

b \ a = {('1', '2', '6', 'b')}

Есть ли какой-нибудь очевидный / питонный способ достижения этого без необходимости вручную перебирать каждый набор и сверяться с каждым tuple[:3]?

Грайдеану Алекс.
источник
3
Моя первоначальная мысль - сделать их классами, определить оператор сравнения
Кенни Остром
2
подкласс setи перезаписать операцию разницы. Я не знаю готового решения, и я сомневаюсь, что оно существует.
Ев. Коунис
Для множеств нет "key = ..." или чего-то подобного (как для sort (..)). Кортежи являются неизменяемыми и хэшируемыми и сравниваются на основе их хешей. Удаление одного элемента приведет к аннулированию хеша. Так что нет - не возможно. Если вам не нужно значение, вы можете создать наборы из 3-х частей:aa = { t[:3] for t in a }
Патрик Артнер
2
@ AK47 Разность (набор) между двумя наборами S и T записывается как S ∖ T, и означает множество, состоящее из элементов S, которые не являются элементами T: x∈S ∖ T⟺x∈S∧x∉T
Грайдеану Алекс.
Подкласс tupleи переопределить оператор разницы
Pynchia

Ответы:

10

Вот как вы можете написать свой собственный класс для переопределения нормального поведения хэширования кортежа:

a_data = [('1', '2', '3', 'a'), ('1', '2', '4', 'a'), ('1', '2', '5', 'b')]
b_data = [('1', '2', '3', 'b'), ('1', '2', '4', 'b'), ('1', '2', '6', 'b')]

class HashableIgnoresLastElement(tuple):
    def __eq__(self, other):
        return self[:-1] == other[:-1]

    def __hash__(self):
        return hash(self[:-1])

a = set(map(HashableIgnoresLastElement, a_data))
b = set(map(HashableIgnoresLastElement, b_data))

print(b - a)

с выходом

{('1', '2', '6', 'b')}

Чтобы изменить поведение наборов кортежей, мы должны изменить способ хэширования кортежей.

Из здесь ,

Объект является хэшируемым, если он имеет хеш-значение, которое никогда не изменяется в течение срока его службы (ему нужен __hash__()метод), и его можно сравнить с другими объектами (ему нужен __eq__()метод). Хэшируемые объекты, которые сравниваются равными, должны иметь одинаковое хеш-значение.

Hashability делает объект пригодным для использования в качестве ключа словаря и члена набора, потому что эти структуры данных используют значение хеша внутри.

Таким образом, чтобы хэширование игнорировало последний элемент, мы должны перегрузить методы dunder __eq__и __hash__соответственно. Это не так сложно, потому что все, что нам нужно сделать, это отрезать последний элемент и затем делегировать его соответствующим методам нормали tuple.

Дальнейшее чтение:

Исаак ван Донген
источник
1
Очень аккуратный! Не могли бы вы также немного рассказать, как это работает? Это может быть полезно тем, кто прочитает это решение.
Грайдеану Алекс.
@GrajdeanuAlex. Я добавил краткое объяснение :). На самом деле это просто объединение кусочков перегрузки операторов и того, как хеширование работает в Python.
Исаак ван Донген
2

Вот один подход определения aи bсо списками , а не комплекты, так как мне кажется , что наиболее прямым решение предполагает индексацию b:

a = [('1', '2', '3', 'a'), ('1', '2', '4', 'a'), ('1', '2', '5', 'b')]
b = [('1', '2', '3', 'b'), ('1', '2', '4', 'b'), ('1', '2', '6', 'b')]

# reconstruct the sets of tuples removing the last elements
a_ = {tuple(t) for *t, _ in a}
b_ = [tuple(t) for *t, _ in b]

# index b based on whether an element in a_
[b[ix] for ix, j in enumerate(b_) if j not in a_]
# [('1', '2', '6', 'b')]
Yatu
источник
1
Это, если я не ошибаюсь, это O (n), так как я использую набор для поиска. Хотя я думаю , что ответ Изаак ван Донген является гораздо более элегантный @konrad
Yatu
1
Вы совершенно правы, использование (и перечисление) списка отбросило меня, но, конечно, разность наборов также должна повторяться в первом наборе.
Конрад Рудольф
1

Наборы работают нормально. Это ваши данные, которые не работают правильно. Если они выглядят по-разному, но на самом деле они одинаковы, тогда определите тип данных, который ведет себя так, как вы хотите. Тогда набор прекрасно работает сам по себе.

class thing:
    def __init__(self, a, b, c, d):
        self.a, self.b, self.c, self.d = a, b, c, d

    def __repr__(self):
        return (str((self.a, self.b, self.c, self.d)))

    def __hash__(self):
        return hash((self.a, self.b, self.c))

    def __eq__(self, other):
        return self.a == other.a and self.b == other.b and self.c == other.c       

a = {thing('1', '2', '3', 'a'), thing('1', '2', '4', 'a'), thing('1', '2', '5', 'b')}
b = {thing('1', '2', '3', 'b'), thing('1', '2', '4', 'b'), thing('1', '2', '6', 'b')}
print (b - a)

{('1', '2', '6', 'b')}

Кенни Остром
источник
3
Вы определили __repr__и __hash__в терминах кортежей, но нет __eq__. Не короче ли и здесь использовать кортежи? Фактически, вы можете использовать нарезку здесь и далее, __hash__чтобы сократить код дальше.
Конрад Рудольф
Да, просто подклассификация кортежа стала большим улучшением в вопросе.
Кенни Остром