Manera pitónica de ignorar el último elemento al hacer la diferencia establecida

11

Digamos que tengo dos 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')}

Ahora, lo que quiero hacer es encontrar la diferencia establecida b \ apero ignorando el último elemento de cada tupla. Entonces es como hacer algo como esto:

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

Rendimiento esperado:

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

¿Hay alguna forma obvia / pitónica de lograr esto sin tener que iterar manualmente sobre cada conjunto y compararlo tuple[:3]?

Grajdeanu Alex.
fuente
3
Mi pensamiento inicial es hacerlos clases, definir operador de comparación
Kenny Ostrom
2
subclase sety sobrescribir la operación de diferencia. No conozco ninguna solución lista para usar y dudo que exista.
Ev. Kounis
No hay "key = ..." o algo similar (como para sort (..)) para conjuntos. Las tuplas son inmutables y hashables y se comparan en función de su hash. Eliminar un elemento anularía el hash. Entonces no, no es posible. Si no necesita el valor, puede crear juegos de 3 partes:aa = { t[:3] for t in a }
Patrick Artner
2
@ AK47 La diferencia (conjunto) entre dos conjuntos S y T se escribe S ∖ T, y significa el conjunto que consta de los elementos de S que no son elementos de T: x∈S ∖ T⟺x∈S∧x∉T
Grajdeanu Alex.
Subclase tupley anulación del operador de diferencia
Pynchia

Respuestas:

10

Así es como puede escribir su propia clase para anular el comportamiento de hash normal de una 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)

con salida

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

Para modificar la forma en que se comportan los conjuntos de tuplas, tenemos que modificar la forma en que se procesan las tuplas.

A partir de aquí ,

Un objeto es hashable si tiene un valor hash que nunca cambia durante su vida útil (necesita un __hash__()método) y puede compararse con otros objetos (necesita un __eq__()método). Los objetos hashables que comparan igual deben tener el mismo valor hash.

Hashability hace que un objeto sea utilizable como clave de diccionario y miembro de conjunto, porque estas estructuras de datos utilizan el valor hash internamente.

Entonces, para que el hash ignore el último elemento, tenemos que sobrecargar los métodos dunder __eq__y de manera __hash__adecuada. Esto no termina siendo tan difícil porque todo lo que tenemos que hacer es cortar el último elemento y luego delegarlo a los métodos apropiados de forma normal tuple.

Otras lecturas:

Izaak van Dongen
fuente
1
¡Muy aseado! ¿Podría también describir un poco cómo funciona esto? Puede valer la pena para quienes lean esta solución.
Grajdeanu Alex.
@GrajdeanuAlex. He agregado una breve explicación :). En realidad, solo combina bits y piezas de sobrecarga del operador y cómo funciona el hash en Python.
Izaak van Dongen
2

Aquí hay un enfoque que define ay bcon listas en lugar de conjuntos, ya que me parece que la solución más directa implica la indexación 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
fuente
1
Esto si no me estoy equivocando es O (n), ya que uso un conjunto para la búsqueda. Aunque creo que la respuesta de Izaak van Dongen es mucho más elegante @konrad
Yatu
1
Tiene toda la razón, el uso de (y la enumeración sobre) una lista me desconcertó, pero, por supuesto, una diferencia establecida también necesita iterar sobre el primer conjunto.
Konrad Rudolph
1

Los juegos funcionan bien. Son sus datos los que no funcionan bien. Si se ven diferentes pero en realidad son lo mismo, defina un tipo de datos que se comporte como usted desea. Luego, el conjunto funciona muy bien por sí solo.

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
fuente
3
Definiste __repr__y __hash__en términos de tuplas, pero no __eq__. ¿No sería más corto usar tuplas aquí también? De hecho, puede usar la división aquí y en __hash__para acortar aún más el código.
Konrad Rudolph
Sí, simplemente subclasificar tuplas fue una gran mejora para la pregunta que se hizo.
Kenny Ostrom