Mapa bidireccional / inverso [duplicado]

101

Estoy haciendo esta cosa de centralita en Python donde necesito hacer un seguimiento de quién está hablando con quién, así que si Alice -> Bob, eso implica que Bob -> Alice.

Sí, podría completar dos mapas hash, pero me pregunto si alguien tiene una idea para hacerlo con uno.

O sugiera otra estructura de datos.

No hay múltiples conversaciones. Digamos que esto es para un centro de llamadas de servicio al cliente, por lo que cuando Alice marca en la centralita, solo va a hablar con Bob. Sus respuestas también van solo para ella.

Sudhir Jonathan
fuente
15
tenga en cuenta que está describiendo un mapa biyectivo.
Nick Dandoulakis
Aquí hay una implementación simple de Diccionario biyectivo , aunque no sé si cumplirá con sus requisitos de desempeño. (Enlace de este artículo de blog sobre boost Bimap para Python , que tiene una buena discusión sobre el tema).
system PAUSE
2
Si Alice está hablando con Bob, supongo que no puede estar hablando también con Charles; ¿Bob tampoco puede hablar con nadie más? Además, ¿cuántas personas y cuántas conversaciones puedes tener en un momento dado?
PAUSA del sistema
No ... no en mi centralita. Cualquier mensaje que me envíe Alice tendrá que ir a Bob. Es solo que estaré enrutando miles de conversaciones simultáneas. Pero cada persona solo habla con una persona a la vez.
Sudhir Jonathan
1
No ... solo necesito enrutar los mensajes del cliente al operador y viceversa ... ni siquiera almacenar las conversaciones de ninguna manera.
Sudhir Jonathan

Respuestas:

90

Puede crear su propio tipo de diccionario subclasificando dicty agregando la lógica que desee. Aquí tienes un ejemplo básico:

class TwoWayDict(dict):
    def __setitem__(self, key, value):
        # Remove any previous connections with these values
        if key in self:
            del self[key]
        if value in self:
            del self[value]
        dict.__setitem__(self, key, value)
        dict.__setitem__(self, value, key)

    def __delitem__(self, key):
        dict.__delitem__(self, self[key])
        dict.__delitem__(self, key)

    def __len__(self):
        """Returns the number of connections"""
        return dict.__len__(self) // 2

Y funciona así:

>>> d = TwoWayDict()
>>> d['foo'] = 'bar'
>>> d['foo']
'bar'
>>> d['bar']
'foo'
>>> len(d)
1
>>> del d['foo']
>>> d['bar']
Traceback (most recent call last):
  File "<stdin>", line 7, in <module>
KeyError: 'bar'

Estoy seguro de que no cubrí todos los casos, pero eso debería ayudarlo a comenzar.

Sasha Chedygov
fuente
2
@SudhirJonathan: Puedes ir mucho más allá con esta idea, por ejemplo, agregar un .addmétodo para que puedas hacer cosas como en d.add('Bob', 'Alice')lugar de usar la sintaxis que mostré. También incluiría algún manejo de errores. Pero entiendes la idea básica. :)
Sasha Chedygov
1
Supongo que esto se incluye en esas adiciones, pero sería útil eliminar los pares de claves antiguos al configurar otros nuevos (también d['foo'] = 'baz'sería necesario eliminar la barclave).
beardc
@TobiasKienzler: Tiene razón, pero esto se incluyó como una suposición en la pregunta.
Sasha Chedygov
5
También vale la pena mencionarlo: la subclasificación dictproduce un comportamiento engañoso aquí, porque si crea el objeto con algún contenido inicial, la estructura se romperá. __init__debe anularse para permitir que una construcción similar d = TwoWayDict({'foo' : 'bar'})funcione correctamente.
Henry Keiter
19
Sólo quiero mencionar que hay una biblioteca para esto: pip install bidict. URL: pypi.python.org/pypi/bidict
user1036719
45

En su caso especial, puede almacenar ambos en un diccionario:

relation = {}
relation['Alice'] = 'Bob'
relation['Bob'] = 'Alice'

Dado que lo que está describiendo es una relación simétrica. A -> B => B -> A

Nadia Alramli
fuente
3
Hmm ... sí, me gusta más este. Estaba tratando de evitar hacer dos entradas, pero esta es la mejor idea hasta ahora.
Sudhir Jonathan
1
Todavía creo que un mapa de dos vías debería ser posible: - /
Sudhir Jonathan
Si tiene que ser eficiente, entonces debajo de las cubiertas necesita que ambas claves estén indexadas en alguna estructura de datos de índice, ya sea un hash, una lista ordenada, un árbol binario, un trie, una matriz de sufijos llena de sistrings o algo incluso más exótico. La forma sencilla de hacerlo en Python es usar un hash.
Kragen Javier Sitaker
@SudhirJonathan Si prefiere un mapa verdaderamente bidireccional, eche un vistazo a bidict como se menciona, por ejemplo, en esta pregunta ; sin embargo , tenga en cuenta los problemas de rendimiento discutidos por Aya en los comentarios sobre mi pregunta de dupe .
Tobias Kienzler
25

Sé que es una pregunta anterior, pero quería mencionar otra gran solución a este problema, a saber, el paquete bidict de Python . Es extremadamente sencillo de usar:

from bidict import bidict
map = bidict(Bob = "Alice")
print(map["Bob"])
print(map.inv["Alice"])
Nearoo
fuente
24

Solo rellenaría un segundo hash, con

reverse_map = dict((reversed(item) for item in forward_map.items()))
Ian Clelland
fuente
7
Tengo algunos paréntesis adicionales allí:reverse_map = dict(reversed(item) for item in forward_map.items())
Andriy Drozdyuk
1
Una manera agradable y fácil si no va a actualizar más el dict. Solíamy_dict.update(dict(reversed(item) for item in my_dict.items()))
Gilly
Al utilizar este código en Python 3 recibo una advertencia: Unexpected type(s): (Generator[Iterator[Union[str, Any]], Any, None]) Possible types: (Mapping) (Iterable[Tuple[Any, Any]]). ¿Alguna idea de cómo deshacerse de la advertencia?
Kerwin Sneijders
9

En realidad, dos mapas hash es probablemente la solución de rendimiento más rápido, suponiendo que pueda ahorrar memoria. Los envolvería en una sola clase: la carga del programador es asegurarse de que dos mapas hash se sincronicen correctamente.

Tríptico
fuente
2
+1, eso es lo que básicamente hace bidict , más azúcar para acceder al mapeo inverso usando mydict[:value]para obtener key(a costa de algo de rendimiento)
Tobias Kienzler
6

Tienes dos problemas distintos.

  1. Tienes un objeto "Conversación". Se refiere a dos personas. Dado que una persona puede tener varias conversaciones, tienes una relación de varios a varios.

  2. Tiene un mapa de persona a una lista de conversaciones. Una conversión tendrá un par de personas.

Haz algo como esto

from collections import defaultdict
switchboard= defaultdict( list )

x = Conversation( "Alice", "Bob" )
y = Conversation( "Alice", "Charlie" )

for c in ( x, y ):
    switchboard[c.p1].append( c )
    switchboard[c.p2].append( c )
S.Lott
fuente
5

No, realmente no hay forma de hacer esto sin crear dos diccionarios. ¿Cómo sería posible implementar esto con un solo diccionario sin dejar de ofrecer un rendimiento comparable?

Es mejor crear un tipo personalizado que encapsule dos diccionarios y exponga la funcionalidad que desea.

Andrew Hare
fuente
3

Una forma menos detallada, todavía usando invertida:

dict(map(reversed, my_dict.items()))
Eti JS
fuente
2

Otra posible solución es implementar una subclase de dict, que contiene el diccionario original y realiza un seguimiento de una versión inversa del mismo. Mantener dos dictados separados puede resultar útil si las claves y los valores se superponen.

class TwoWayDict(dict):
    def __init__(self, my_dict):
        dict.__init__(self, my_dict)
        self.rev_dict = {v : k for k,v in my_dict.iteritems()}

    def __setitem__(self, key, value):
        dict.__setitem__(self, key, value)
        self.rev_dict.__setitem__(value, key)

    def pop(self, key):
        self.rev_dict.pop(self[key])
        dict.pop(self, key)

    # The above is just an idea other methods
    # should also be overridden. 

Ejemplo:

>>> d = {'a' : 1, 'b' : 2} # suppose we need to use d and its reversed version
>>> twd = TwoWayDict(d)    # create a two-way dict
>>> twd
{'a': 1, 'b': 2}
>>> twd.rev_dict
{1: 'a', 2: 'b'}
>>> twd['a']
1
>>> twd.rev_dict[2]
'b'
>>> twd['c'] = 3    # we add to twd and reversed version also changes
>>> twd
{'a': 1, 'c': 3, 'b': 2}
>>> twd.rev_dict
{1: 'a', 2: 'b', 3: 'c'}
>>> twd.pop('a')   # we pop elements from twd and reversed  version changes
>>> twd
{'c': 3, 'b': 2}
>>> twd.rev_dict
{2: 'b', 3: 'c'}
Akavall
fuente
2

Existe la biblioteca de colecciones extendidas en pypi: https://pypi.python.org/pypi/collections-extended/0.6.0

Usar la clase de biyección es tan fácil como:

RESPONSE_TYPES = bijection({
    0x03 : 'module_info',
    0x09 : 'network_status_response',
    0x10 : 'trust_center_device_update'
})
>>> RESPONSE_TYPES[0x03]
'module_info'
>>> RESPONSE_TYPES.inverse['network_status_response']
0x09
Schwolop
fuente
2

Me gusta la sugerencia de bidict en uno de los comentarios.

pip install bidict

Uso:

# This normalization method should save hugely as aDaD ~ yXyX have the same form of smallest grammar.
# To get back to your grammar's alphabet use trans

def normalize_string(s, nv=None):
    if nv is None:
        nv = ord('a')
    trans = bidict()
    r = ''
    for c in s:
        if c not in trans.inverse:
            a = chr(nv)
            nv += 1
            trans[a] = c
        else:
            a = trans.inverse[c]
        r += a
    return r, trans


def translate_string(s, trans):
    res = ''
    for c in s:
        res += trans[c]
    return res


if __name__ == "__main__":
    s = "bnhnbiodfjos"

    n, tr = normalize_string(s)
    print(n)
    print(tr)
    print(translate_string(n, tr))    

Dado que no hay muchos documentos al respecto. Pero tengo todas las características que necesito para que funcionen correctamente.

Huellas dactilares:

abcbadefghei
bidict({'a': 'b', 'b': 'n', 'c': 'h', 'd': 'i', 'e': 'o', 'f': 'd', 'g': 'f', 'h': 'j', 'i': 's'})
bnhnbiodfjos
AlgebraicaGeometríaEstudiante
fuente
1

El módulo de extensión kjbuckets C proporciona una estructura de datos de "gráfico" que creo que le da lo que desea.

Kragen Javier Sitaker
fuente
Lo siento, no lo mencioné, pero está en el motor de la aplicación ... así que no hay extensiones C.
Sudhir Jonathan
1

Aquí hay una implementación más de diccionario bidireccional al extender la dictclase de pitones en caso de que no le guste ninguno de los otros:

class DoubleD(dict):
    """ Access and delete dictionary elements by key or value. """ 

    def __getitem__(self, key):
        if key not in self:
            inv_dict = {v:k for k,v in self.items()}
            return inv_dict[key]
        return dict.__getitem__(self, key)

    def __delitem__(self, key):
        if key not in self:
            inv_dict = {v:k for k,v in self.items()}
            dict.__delitem__(self, inv_dict[key])
        else:
            dict.__delitem__(self, key)

Úselo como un diccionario de Python normal, excepto en la construcción:

dd = DoubleD()
dd['foo'] = 'bar'
browlm13
fuente
1

Una forma en la que me gusta hacer este tipo de cosas es algo como:

{my_dict[key]: key for key in my_dict.keys()}
Tobi Abiodun
fuente
1
¡Bienvenido a StackOverflow! Por favor, editar su respuesta y añadir más explicación para el código, preferentemente, además de darle la descripción de por qué es diferente de los otros 14 respuestas. La pregunta tiene más de diez años y ya tiene una respuesta aceptada y muchas bien explicadas y bien votadas. Sin más detalles en su publicación, es de calidad comparativamente menor y probablemente será rechazada o eliminada. Agregar esa información adicional ayudará a justificar la existencia de su respuesta.
Das_Geek