Maneira pitônica de ignorar o último elemento ao fazer a diferença definida

11

Digamos que eu tenho dois set()s:

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

Agora, o que eu quero fazer é encontrar a diferença definida, b \ amas ignorando o último elemento de cada tupla. Então é como fazer algo assim:

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

Saída esperada:

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

Existe alguma maneira óbvia / pitônica de conseguir isso sem ter que iterar manualmente sobre cada conjunto e comparar com cada um tuple[:3]?

Grajdeanu Alex.
fonte
3
Meu pensamento inicial é torná-los aulas, define operador de comparação
Kenny Ostrom
2
subclasse sete substitua a operação de diferença. Não tenho uma solução pronta para uso e duvido que exista.
Ev. Kounis
Não existe "chave = ..." ou algo parecido (como no tipo (..)) para conjuntos. As tuplas são imutáveis ​​e hashable e são comparadas com base no hash. A remoção de um elemento anularia o hash. Então não - não é possível. Se você não precisar do valor, poderá criar conjuntos de 3 partes:aa = { t[:3] for t in a }
Patrick Artner 18/12/19
2
@ AK47 A diferença (conjunto) entre dois conjuntos S e T é escrita S ∖ T e significa o conjunto que consiste nos elementos de S que não são elementos de T: x∈S ∖ T⟺x∈S∧x∉T
Grajdeanu Alex.
Subclasse tuplee substitua o operador de diferença
Pynchia

Respostas:

10

Veja como você pode escrever sua própria classe para substituir o comportamento de hash normal de uma tupla:

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)

com saída

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

Para modificar a maneira como os conjuntos de tuplas se comportam, precisamos modificar a maneira como as tuplas são hash.

A partir daqui ,

Um objeto é lavável se tiver um valor de hash que nunca seja alterado durante sua vida útil (precisa de um __hash__()método) e poderá ser comparado a outros objetos (precisa de um__eq__() método). Objetos hashable que comparam iguais devem ter o mesmo valor de hash.

A capacidade de Hashability torna um objeto utilizável como chave de dicionário e membro do conjunto, porque essas estruturas de dados usam o valor de hash internamente.

Portanto, para fazer com que o hash ignore o último elemento, precisamos sobrecarregar os métodos dunder __eq__e __hash__adequadamente. Isso não acaba sendo tão difícil, porque tudo o que precisamos fazer é cortar o último elemento e, em seguida, delegar aos métodos apropriados de um método normal.tuple .

Leitura adicional:

Izaak van Dongen
fonte
11
Muito arrumado! Você também pode descrever um pouco como isso funciona? Pode valer a pena para quem ler esta solução.
Grajdeanu Alex.
@GrajdeanuAlex. Eu adicionei uma breve explicação :). Realmente, é apenas a combinação de bits de sobrecarga do operador e como o hash funciona no Python.
Izaak van Dongen
2

Aqui está uma abordagem que define ae bcom listas em vez de conjuntos, pois me parece que a solução mais direta implica a indexação 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
fonte
11
Isso se não me engano é O (n), pois uso um conjunto para a pesquisa. Embora eu acho que a resposta de Izaak van Dongen é muito mais elegante @ Konrad
yatu
11
Você está totalmente certo, o uso (e a enumeração acima) de uma lista me deixou desconcertado, mas é claro que uma diferença de conjunto também precisa iterar no primeiro conjunto.
Konrad Rudolph
1

Conjuntos funcionam bem. São seus dados que não funcionam corretamente. Se eles parecerem diferentes, mas na verdade forem iguais, defina um tipo de dados que se comporte como você deseja. Em seguida, o conjunto funciona muito bem por conta própria.

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

Kenny Ostrom
fonte
3
Você definiu __repr__e __hash__em termos de tuplas, mas não __eq__. Também não seria mais curto usar tuplas aqui? Na verdade, você pode usar o fatiamento aqui e __hash__para diminuir ainda mais o código.
Konrad Rudolph
Sim, apenas a subclasse de tupla foi uma grande melhoria para a pergunta, conforme solicitado.
Kenny Ostrom