Mapa bidirecional / reverso [duplicado]

101

Estou fazendo essa coisa de mesa telefônica em python em que preciso manter o controle de quem está falando com quem, então se Alice -> Bob, isso implica que Bob -> Alice.

Sim, eu poderia preencher dois mapas hash, mas estou me perguntando se alguém tem uma ideia de fazer isso com um.

Ou sugira outra estrutura de dados.

Não existem várias conversas. Digamos que isso seja para um call center de atendimento ao cliente, então, quando Alice discar para a mesa telefônica, ela falará apenas com Bob. Suas respostas também vão apenas para ela.

Sudhir Jonathan
fonte
15
observe que você está descrevendo um mapa bijetivo.
Nick Dandoulakis,
Aqui está uma implementação simples do Dicionário bijetivo , embora eu não saiba se ela atenderá aos seus requisitos de desempenho. (Link deste artigo de blog sobre boost Bimap for Python , que tem uma boa discussão sobre o assunto.)
PAUSA do sistema
2
Se Alice está falando com Bob, presumo que ela não pode estar falando também com Charles; nem Bob pode estar falando com mais ninguém? Além disso, quantas pessoas e quantas conversas você pode ter em um determinado momento?
PAUSA do sistema
Nah ... não na minha mesa telefônica. Qualquer mensagem que Alice me enviar terá que ir para Bob. É só que vou direcionar milhares de conversas simultâneas. Mas cada pessoa fala apenas com uma outra pessoa por vez.
Sudhir Jonathan
1
Não ... só preciso encaminhar as mensagens do cliente para a operadora e vice-versa ... nem mesmo armazenando as conversas de forma alguma.
Sudhir Jonathan,

Respostas:

90

Você pode criar seu próprio tipo de dicionário criando uma subclasse dicte adicionando a lógica que deseja. Aqui está um exemplo 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

E funciona assim:

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

Tenho certeza de que não cobri todos os casos, mas isso deve ajudar você a começar.

Sasha Chedygov
fonte
2
@SudhirJonathan: Você pode ir muito mais longe com essa ideia - por exemplo, adicione um .addmétodo para fazer coisas como, em d.add('Bob', 'Alice')vez de usar a sintaxe que mostrei. Eu também incluiria algum tratamento de erros. Mas você entendeu a ideia básica. :)
Sasha Chedygov
1
Acho que isso se enquadra nessas adições, mas seria útil excluir pares de chaves antigos ao definir novos ( d['foo'] = 'baz'seria necessário remover a barchave adicionalmente ).
beardc
@TobiasKienzler: Você está correto, mas isso foi listado como uma suposição na pergunta.
Sasha Chedygov
5
Também vale a pena mencionar: a subclasse dictproduz algum comportamento enganoso aqui, pois se você criar o objeto com algum conteúdo inicial, a estrutura será quebrada. __init__precisa ser substituído para permitir que uma construção d = TwoWayDict({'foo' : 'bar'})funcione corretamente.
Henry Keiter
19
Só quero mencionar que há uma biblioteca para isso: pip install bidict. URL: pypi.python.org/pypi/bidict
user1036719
45

No seu caso especial, você pode armazenar ambos em um dicionário:

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

Já que o que você está descrevendo é uma relação simétrica. A -> B => B -> A

Nadia Alramli
fonte
3
Hmm ... sim, eu gosto mais deste. Estava tentando evitar fazer duas entradas, mas essa é a melhor ideia até agora.
Sudhir Jonathan
1
Ainda acho que um mapa de mão dupla deveria ser possível: - /
Sudhir Jonathan
Se tiver que ser eficiente, então, nos bastidores, você precisa que ambas as chaves sejam indexadas em alguma estrutura de dados de índice - seja um hash, uma lista classificada, uma árvore binária, um trie, um array de sufixos cheio de sistrings ou algo parecido mais exótico. A maneira simples de fazer isso em Python é usar um hash.
Kragen Javier Sitaker,
@SudhirJonathan Se você preferir um mapa realmente bidirecional, dê uma olhada no bidict conforme mencionado, por exemplo, nesta pergunta - observe os problemas de desempenho discutidos por Aya nos comentários sobre minha pergunta sobre o dupe .
Tobias Kienzler
25

Eu sei que é uma pergunta mais antiga, mas gostaria de mencionar outra ótima solução para esse problema, ou seja, o bidict do pacote python . É extremamente simples de usar:

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

Eu apenas preencheria um segundo hash, com

reverse_map = dict((reversed(item) for item in forward_map.items()))
Ian Clelland
fonte
7
Tenho alguns colchetes extras lá:reverse_map = dict(reversed(item) for item in forward_map.items())
Andriy Drozdyuk
1
Boa maneira fácil se você não for atualizar o dicionário. Eu useimy_dict.update(dict(reversed(item) for item in my_dict.items()))
Gilly
Ao usar esse código em Python 3 Eu começo um aviso: Unexpected type(s): (Generator[Iterator[Union[str, Any]], Any, None]) Possible types: (Mapping) (Iterable[Tuple[Any, Any]]). Alguma ideia de como se livrar do aviso?
Kerwin Sneijders
9

Dois mapas de hash são provavelmente a solução de desempenho mais rápido, supondo que você possa poupar memória. Eu os envolveria em uma única classe - a responsabilidade do programador é garantir que dois mapas de hash sejam sincronizados corretamente.

Tríptico
fonte
2
+1, Isso é o que o bidict basicamente faz, mais açúcar para acessar o mapeamento inverso usando mydict[:value]para obter key(ao custo de algum desempenho)
Tobias Kienzler
6

Você tem dois problemas separados.

  1. Você tem um objeto "Conversa". Refere-se a duas Pessoas. Como uma pessoa pode ter várias conversas, você tem um relacionamento de muitos para muitos.

  2. Você tem um mapa de pessoa para uma lista de conversas. Uma conversão terá um par de pessoas.

Faça algo assim

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
fonte
5

Não, realmente não há como fazer isso sem criar dois dicionários. Como seria possível implementar isso com apenas um dicionário e, ao mesmo tempo, oferecer desempenho comparável?

É melhor criar um tipo personalizado que encapsule dois dicionários e exponha a funcionalidade desejada.

Andrew Hare
fonte
3

Uma forma menos prolixa, ainda usando o reverso:

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

Outra solução possível é implementar uma subclasse de dict, que contém o dicionário original e mantém o controle de uma versão reversa dele. Manter dois dictes separados pode ser útil se as chaves e os valores estiverem sobrepostos.

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. 

Exemplo:

>>> 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
fonte
2

Existe a biblioteca de coleções estendidas em pypi: https://pypi.python.org/pypi/collections-extended/0.6.0

Usar a classe bijection é tão fácil quanto:

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
fonte
2

Gostei da sugestão do bidict em um dos comentários.

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

Já que não há muitos documentos sobre isso. Mas tenho todos os recursos de que preciso funcionando corretamente.

Impressões:

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

O módulo de extensão kjbuckets C fornece uma estrutura de dados em "gráfico" que acredito dar a você o que você deseja.

Kragen Javier Sitaker
fonte
Desculpe, não mencionei isso, mas está no App Engine ... então não há extensões C.
Sudhir Jonathan,
1

Aqui está mais uma implementação de dicionário bidirecional, estendendo a dictclasse pythons , caso você não goste de nenhuma das outras:

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)

Use-o como um dicionário python normal, exceto na construção:

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

Uma maneira que gosto de fazer esse tipo de coisa é algo como:

{my_dict[key]: key for key in my_dict.keys()}
Tobi Abiodun
fonte
1
Bem-vindo ao StackOverflow! Por favor edite sua resposta e adicionar mais explicação para seu código, de preferência, também acrescentando descrição a respeito de porque ele é diferente dos outros 14 respostas. A pergunta tem mais de dez anos , e já tem uma resposta aceita e muitas bem explicadas e bem votadas. Sem mais detalhes em sua postagem, é de qualidade comparativamente inferior e provavelmente será rejeitado ou removido. Adicionar essa informação extra ajudará a justificar a existência de sua resposta.
Das_Geek