_NOTFOUND = object() class Flipdict(dict): """An injective (one-to-one) python dict. Ensures that each key maps to a unique value, and each value maps back to that same key. A Flipdict is a dict, implementing the dict API:: >>> fd = Flipdict({1: 'one', 2: 'two', 3: 'three'}) >>> fd[1] 'one' >>> fd.setdefault(4, 'four') 'four' >>> fd Flipdict({1: 'one', 2: 'two', 3: 'three', 4: 'four'}) Each Flipdict has a "flip" attribute, its inverse mapping:: >>> fd is fd.flip.flip True >>> sorted(fd.flip.items()) [('four', 4), ('one', 1), ('three', 3), ('two', 2)] Just like a dict, remapping a key will erase the old mapping. IMPORTANT: Suppose a Flipdict maps key K to value V. Then mapping a *new* key to the value V will raise KeyError:: >>> fd[4444] = 'four' Traceback (most recent call last): .... KeyError: "(key,val) would erase mapping for value 'four'" This avoids a subtle situation. The alternative is to silently erase the existing pair (K,V)! Instead, explicitly use the "flip" attribute like a python dict:: >>> fd.flip['four'] = 4444 >>> fd Flipdict({1: 'one', 2: 'two', 3: 'three', 4444: 'four'}) """ def __init__(self, *args, **kw): """Cf. dict.__init__ Examples:: >>> fd = Flipdict( zip((1, 2, 3), ('one', 'two', 'three')) ) >>> fd == Flipdict({1: 'one', 2: 'two', 3: 'three'}) True >>> fd == Flipdict(one=1, two=2, three=3).flip True IMPORTANT: Mapping many keys to the same value will raise KeyError. """ self._flip = dict.__new__(self.__class__) setattr(self._flip, "_flip", self) for key, val in dict(*args, **kw).iteritems(): self[key] = val @property def flip(self): """The inverse mapping. Examples:: >>> fd = Flipdict({1: 'one', 2: 'two', 3: 'three'}) >>> fd.flip['two'] 2 >>> fd is fd.flip.flip True """ return self._flip #{ Non-mutating methods that are NOT delegated to the dict superclass. def __repr__(self): return "%s(%r)" % (self.__class__.__name__, dict(self)) __str__ = __repr__ def copy(self): return self.__class__(self) @classmethod def fromkeys(cls, keys, value=None): """ NOTE: This method is mostly useless for a Flipdict, because each key must map to a UNIQUE value. """ return cls(dict.fromkeys(keys, value)) #} #{ Mutating methods. These must keep the inverse mapping in sync! def __setitem__(self, key, val): """self[key] = val (and self.flip[val] = key) Remapping a key will erase the old mapping (just like a dict):: >>> fd = Flipdict(one=1, two=2) >>> fd['two'] = 22 >>> fd Flipdict({'two': 22, 'one': 1}) >>> fd.flip Flipdict({1: 'one', 22: 'two'}) Just like a dict, remapping a key will erase the old mapping. IMPORTANT: Suppose a Flipdict maps key K to value V. Then mapping a *new* key to the value V will raise KeyError:: >>> fd['twenty-two'] = 22 Traceback (most recent call last): .... KeyError: '(key,val) would erase mapping for value 22' This avoids a subtle situation. The alternative is to silently erase the existing pair (K,V)! Instead, explicitly use the "flip" attribute like a python dict:: >>> fd.flip[22] = 'twenty-two' >>> sorted(fd.items()) [('one', 1), ('twenty-two', 22)] """ k = self._flip.get(val, _NOTFOUND) if not (k is _NOTFOUND or k==key): raise KeyError('(key,val) would erase mapping for value %r' % val) v = self.get(key, _NOTFOUND) if v is not _NOTFOUND: dict.__delitem__(self._flip, v) dict.__setitem__(self, key, val) dict.__setitem__(self._flip, val, key) def setdefault(self, key, default = None): # Copied from python's UserDict.DictMixin code. try: return self[key] except KeyError: self[key] = default return default def update(self, other = None, **kwargs): # Copied from python's UserDict.DictMixin code. # Make progressively weaker assumptions about "other" if other is None: pass elif hasattr(other, 'iteritems'): # iteritems saves memory and lookups for k, v in other.iteritems(): self[k] = v elif hasattr(other, 'keys'): for k in other.keys(): self[k] = other[k] else: for k, v in other: self[k] = v if kwargs: self.update(kwargs) def __delitem__(self, key): val = dict.pop(self, key) dict.__delitem__(self._flip, val) def pop(self, key, *args): val = dict.pop(self, key, *args) dict.__delitem__(self._flip, val) return val def popitem(self): key, val = dict.popitem(self) dict.__delitem__(self._flip, val) return key, val def clear(self): dict.clear(self) dict.clear(self._flip) def makepair(*args, **kw): """Returns a pair: the Flipdict(*args, **kw) and its inverse. Example:: >>> chr2num, num2chr = makepair({'a':1, 'b':2, 'c':3}) >>> chr2num['z'] = 26 >>> num2chr.items() [(1, 'a'), (2, 'b'), (3, 'c'), (26, 'z')] """ fd = Flipdict(*args, **kw) return fd, fd.flip if __name__=='__main__': import doctest doctest.testmod(optionflags=doctest.ELLIPSIS)