125

I'm doing this switchboard thing in python where I need to keep track of who's talking to whom, so if Alice --> Bob, then that implies that Bob --> Alice.

Yes, I could populate two hash maps, but I'm wondering if anyone has an idea to do it with one.

Or suggest another data structure.

There are no multiple conversations. Let's say this is for a customer service call center, so when Alice dials into the switchboard, she's only going to talk to Bob. His replies also go only to her.

Peter O.
  • 32,158
  • 14
  • 82
  • 96
Sudhir Jonathan
  • 16,998
  • 13
  • 66
  • 90
  • 26
    note that you are describing a bijective map. – Nick Dandoulakis Sep 21 '09 at 19:47
  • 2
    If Alice is talking to Bob, I take it that she can't also be talking to Charles; nor can Bob be talking to anyone else? Also, how many people and how many conversations can you have at any given time? – system PAUSE Sep 21 '09 at 20:15
  • Nah... not on my switchboard. Any message that alice sends me will have to go to Bob. Its just I'll be routing thousands of simultaneous conversations. But each person only talks to one other person at a time. – Sudhir Jonathan Sep 21 '09 at 20:31
  • Perhaps what you need is a Conversation class which has attributes including operator_id and customer_id, plus two maps: operator_id -> conversation, and customer_id -> conversation. – John Machin Sep 22 '09 at 02:30
  • 1
    No... I just need to route the customer's messages to the operator and vice versa... not even storing the conversations in any way. – Sudhir Jonathan Sep 22 '09 at 05:42
  • If you're looking for a more general case (non necessarily bijective), see [How to implement an efficient bidirectional hash table?](https://stackoverflow.com/questions/3318625/how-to-implement-an-efficient-bidirectional-hash-table). – Basj Aug 21 '19 at 07:42

15 Answers15

112

You can create your own dictionary type by subclassing dict and adding the logic that you want. Here's a basic example:

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

And it works like so:

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

I'm sure I didn't cover all the cases, but that should get you started.

Sasha Chedygov
  • 127,549
  • 26
  • 102
  • 115
  • 2
    @SudhirJonathan: You can go much further with this idea--for instance, add an `.add` method so that you can do things like `d.add('Bob', 'Alice')` instead of using the syntax I showed. I would also include some error handling. But you get the basic idea. :) – Sasha Chedygov Nov 07 '12 at 22:46
  • 1
    I guess this falls under those additions, but it'd be helpful to delete old key pairs when setting new ones (`d['foo'] = 'baz'` would need to additionally remove the `bar` key). – beardc Jan 29 '13 at 19:10
  • @TobiasKienzler: You are correct, but this was listed as an assumption in the question. – Sasha Chedygov May 28 '13 at 17:28
  • @SashaChedygov I didn't read that out of the question, only that it's an bijective mapping - but you're right, in this case the mapping is from the set of participants to itself. So [this question](http://stackoverflow.com/questions/12527724/build-a-dictionary-for-find-key-by-value) which does not have that assumption is a fupe (=fake-dupe) – Tobias Kienzler May 29 '13 at 07:07
  • 5
    Also worth a mention: subclassing `dict` produces some misleading behavior here, because if you create the object with some initial content, the structure will be broken. `__init__` needs to be overridden to allow a construction like `d = TwoWayDict({'foo' : 'bar'})` to work properly. – Henry Keiter Jun 04 '14 at 17:20
  • 21
    Just want to mention that there is a library for this: `pip install bidict`. URL: https://pypi.python.org/pypi/bidict/ – user1036719 Mar 24 '15 at 06:44
  • following @Henry Keither's comment, i added the init function `def __init__(self, seq=None, **kwargs): if seq is None: super(TwoWayDict,self).__init__(**kwargs) else: super(TwoWayDict,self).__init__(seq, **kwargs) for k,v in seq.iteritems(): dict.__setitem__(self, v, k) for k,v in kwargs.iteritems(): dict.__setitem__(self, v, k)` – JoeyZhao Nov 29 '17 at 22:40
  • Both this answer and http://pypi.python.org/pypi/bidict fail in the case where **two keys have same value**, example: `d['a'] = 1; d['b] = 1`. In this case, [here is a general solution](https://stackoverflow.com/a/21894086/1422096). – Basj Jun 19 '18 at 17:45
  • I wrote my own BiDict based on what i've seen here and without {key -> key} issue. Also my implementation has same methods and behavior, that you expect from a dict. Please ping me here if you find some uncovered cases: gist.github.com/titovanton/2225805da5ccdd788349bbf7f1eef1a8 – AntonTitov Sep 29 '20 at 10:40
  • Just want to mention IT WILL NOT work as intended: in case of intersection between possibles content in `keys` and `values` – Alkor Apr 18 '22 at 10:05
58

In your special case you can store both in one dictionary:

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

Since what you are describing is a symmetric relationship. A -> B => B -> A

Nadia Alramli
  • 111,714
  • 37
  • 173
  • 152
  • 4
    Hmm... yeah, I like this one the best. Was trying to avoid making two entries, but this is the best idea so far. – Sudhir Jonathan Sep 21 '09 at 20:32
  • 2
    Still think a two way map ought to be possible :-/ – Sudhir Jonathan Sep 21 '09 at 20:37
  • If it has to be efficient, then under the covers you need both keys to be indexed in some index data structure — whether that's a hash, a sorted list, a binary tree, a trie, a suffix array full of sistrings, or something even more exotic. The simple way to do that in Python is to use a hash. – Kragen Javier Sitaker Sep 22 '09 at 19:32
  • @SudhirJonathan If you prefer a truly two way map, have a look at [bidict](http://pypi.python.org/pypi/bidict/0.1.1) as mentioned e.g. in [this question](http://stackoverflow.com/questions/3318625/efficient-bidirectional-hash-table-in-python) - note the performance issues discussed by [Aya](http://stackoverflow.com/users/172176/aya) in the comments on [my dupe question](http://stackoverflow.com/questions/16793526/is-there-a-better-way-to-store-a-twoway-dictionary-than-storing-its-inverse-sepa/16793711) though. – Tobias Kienzler May 29 '13 at 07:16
38

I know it's an older question, but I wanted to mention another great solution to this problem, namely the python package bidict. It's extremely straight forward to use:

from bidict import bidict
map = bidict(Bob = "Alice")
print(map["Bob"])
print(map.inv["Alice"])
MaxNoe
  • 14,470
  • 3
  • 41
  • 46
Nearoo
  • 4,454
  • 3
  • 28
  • 39
26

I would just populate a second hash, with

reverse_map = dict((reversed(item) for item in forward_map.items()))
Ian Clelland
  • 43,011
  • 8
  • 86
  • 87
  • 7
    Got some extra brackets in there: `reverse_map = dict(reversed(item) for item in forward_map.items())` – Andriy Drozdyuk Dec 12 '11 at 17:30
  • 1
    Nice easy way if you won't be further updating the dict. I used `my_dict.update(dict(reversed(item) for item in my_dict.items()))` – Gilly Jun 01 '17 at 01:50
  • When using this code in Python 3 I get a warning : `Unexpected type(s): (Generator[Iterator[Union[str, Any]], Any, None]) Possible types: (Mapping) (Iterable[Tuple[Any, Any]])`. Any ideas how to get rid of the warning? – Kerwin Sneijders Jan 08 '20 at 00:38
  • In python3, no warning with this: `my_dict.update({item[1]: item[0] for item in my_dict.items()})` – mmindenhall Oct 23 '20 at 17:51
14

Two hash maps is actually probably the fastest-performing solution assuming you can spare the memory. I would wrap those in a single class - the burden on the programmer is in ensuring that two the hash maps sync up correctly.

Kenan Banks
  • 207,056
  • 34
  • 155
  • 173
  • 2
    +1, That's what [bidict](http://pypi.python.org/pypi/bidict/0.1.1) basically does, plus sugar for accessing the inverse mapping by using `mydict[:value]` to obtain `key` (at the cost of some performance) – Tobias Kienzler May 29 '13 at 07:18
9

A less verbose way, still using reversed:

dict(map(reversed, my_dict.items()))
Eti JS
  • 151
  • 1
  • 4
6

You have two separate issues.

  1. You have a "Conversation" object. It refers to two Persons. Since a Person can have multiple conversations, you have a many-to-many relationship.

  2. You have a Map from Person to a list of Conversations. A Conversion will have a pair of Persons.

Do something like this

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
  • 384,516
  • 81
  • 508
  • 779
5

No, there is really no way to do this without creating two dictionaries. How would it be possible to implement this with just one dictionary while continuing to offer comparable performance?

You are better off creating a custom type that encapsulates two dictionaries and exposes the functionality you want.

Andrew Hare
  • 344,730
  • 71
  • 640
  • 635
2

You may be able to use a DoubleDict as shown in recipe 578224 on the Python Cookbook.

Noctis Skytower
  • 21,433
  • 16
  • 79
  • 117
2

Another possible solution is to implement a subclass of dict, that holds the original dictionary and keeps track of a reversed version of it. Keeping two seperate dicts can be useful if keys and values are overlapping.

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. 

Example:

>>> 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
  • 82,592
  • 51
  • 207
  • 251
2

There's the collections-extended library on pypi: https://pypi.python.org/pypi/collections-extended/0.6.0

Using the bijection class is as easy as:

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
  • 21
  • 2
2

I like the suggestion of bidict in one of the comments.

pip install bidict

Useage:

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

Since there aren't much docs about it. But I've got all the features I need from it working correctly.

Prints:

abcbadefghei
bidict({'a': 'b', 'b': 'n', 'c': 'h', 'd': 'i', 'e': 'o', 'f': 'd', 'g': 'f', 'h': 'j', 'i': 's'})
bnhnbiodfjos
MathCrackExchange
  • 595
  • 1
  • 6
  • 25
1

The kjbuckets C extension module provides a "graph" data structure which I believe gives you what you want.

1

Here's one more two-way dictionary implementation by extending pythons dict class in case you didn't like any of those other ones:

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 it as a normal python dictionary except in construction:

dd = DoubleD()
dd['foo'] = 'bar'
browlm13
  • 21
  • 2
1

A way I like to do this kind of thing is something like:

{my_dict[key]: key for key in my_dict.keys()}
  • 1
    Welcome to StackOverflow! Please [edit your answer](https://stackoverflow.com/posts/58978967/edit) and add more explanation for your code, preferably also adding description as to why it's different from the 14 other answers. The question is over *ten years old*, and already has an accepted answer and many well-explained, well-upvoted ones as well. Without more detail in your post, it is of comparatively lower quality and will probably get downvoted or removed. Adding that extra info will help justify your answer's existence. – Das_Geek Nov 21 '19 at 16:22