bidict/bidict.py

866 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Overview
--------
:mod:`bidict` provides a bidirectional mapping data structure and related
functionality to naturally work with one-to-one relations in Python.
Unlike alternative implementations, ``bidict`` builds on top of the dict
API and supports the familiar ``__getitem__`` syntax. It also supports a
convenient slice syntax to express an inverse mapping::
>>> element_by_symbol = bidict(H='hydrogen')
>>> element_by_symbol['H'] # forward mapping works just like with dict
'hydrogen'
>>> element_by_symbol[:'hydrogen'] # use slice for the inverse mapping
'H'
Syntax hacks ftw.
Motivation
----------
Python's built-in dict lets us associate unique keys with arbitrary values.
Because keys must be hashable, values can be looked up by key in constant time.
Different keys can map to the same value, but a single key cannot map to two
different values. For instance, ``{-1: 1, 0: 0, 1: 1}`` is a dict with
three unique keys and two unique values, because the keys -1 and 1 both map to
1. If you try to write its inverse ``{1: -1, 0: 0, 1: 1}``, the dict that
results has only two mappings, one for key 1 and one for key 0; since key 1
is not allowed to map to both -1 and 1, one of these mappings is discarded.
Sometimes the relation we're modeling will only ever have a single key mapping
to a single value, as in the relation of chemical elements and their symbols.
This is called a one-to-one (or injective) mapping (see
https://en.wikipedia.org/wiki/Injective_mapping).
In this case we can be sure that the inverse mapping has the same number of
items as the forward mapping, and moreover that if key k maps to value v in the
forward mapping, then v maps to k in the inverse. It would be useful then
to be able to look up, in constant time, keys by value, in addition to being
able to look up values by key. With the additional constraint that values must
also be hashable as well as keys, we can get constant-time forward and inverse
lookups -- with a convenient syntax to boot -- with ``bidict``.
More Examples
-------------
Expanding on the previous example, anywhere the ``__getitem__`` syntax can be
used to reference a forward mapping, slice syntax can be used too::
>>> element_by_symbol['H'] = 'Hydrogen'
>>> element_by_symbol['H':]
'Hydrogen'
Including setting and deleting items in either direction::
>>> element_by_symbol['He':] = 'helium'
>>> element_by_symbol[:'lithium'] = 'Li'
>>> del element_by_symbol['H':]
>>> del element_by_symbol[:'lithium']
>>> element_by_symbol
bidict({'He': 'helium'})
The rest of the ``MutableMapping`` interface is also supported::
>>> 'C' in element_by_symbol
False
>>> element_by_symbol.get('C', 'carbon')
'carbon'
>>> element_by_symbol.pop('He')
'helium'
>>> element_by_symbol
bidict({})
>>> element_by_symbol.update(Hg='mercury')
>>> element_by_symbol
bidict({'Hg': 'mercury'})
You can also use the unary inverse operator ~ on a ``bidict`` to get the
inverse mapping::
>>> ~element_by_symbol
bidict({'mercury': 'Hg'})
This is especially handy for composing with other operations::
>>> 'mercury' in ~element_by_symbol
True
>>> (~element_by_symbol).pop('mercury')
'Hg'
Because ``bidict`` maintains inverse mappings alongside forward mappings,
getting the inverse or composing an operation with the inverse costs only
constant time; the inverse does not have to be computed from scratch.
See the :class:`bidict.bidict` class for more documentation.
The ``inverted`` iterator is also provided in the spirit of the built-in
function ``reversed``. Pass in a mapping to get the inverse mapping, an
iterable of pairs to get the pairs' inverses, or any object implementing an
``__inverted__`` method. See the :class:`bidict.inverted` class for more
documentation.
``frozenbidict`` provides a hashable, immutable version of ``bidict`` making it
possible to insert into sets or mappings. See the :class:`bidict.frozenbidict`
class for more documentation.
The ``namedbidict`` class factory can be used to create a bidirectional mapping
with customized names for the forward and the inverse mappings accessible via
attributes. See :attr:`bidict.namedbidict` for more documentation.
Notes
-----
* It is intentional that the term "inverse" is used rather than "reverse".
Consider a collection of (k, v) pairs. Taking the reverse of the collection
can only be done if it is ordered, and (as the name says) reverses the order
of the pairs in the collection. But, each original (k, v) pair remains in the
resulting collection. By contrast, taking the inverse of such a collection
does not require an original ordering, and does not make any claims about the
ordering of the result, but rather just replaces every (k, v) pair with the
inverse pair (v, k).
* "keys" and "values" could perhaps more properly be called "primary keys" and
"secondary keys" (as in a database), or even "forward keys" and "inverse
keys", respectively. ``bidict`` sticks with the terms "keys" and "values" for
the sake of familiarity and to avoid potential confusion, but it's worth
noting that values are also keys themselves. This allows us to return a
set-like object for :attr:`bidict.BidirectionalMapping.values` (Python 3)
/ :attr:`bidict.BidirectionalMapping.viewvalues` (Python 2), for example.
* The built-in ``htmlentitydefs`` module provides an example of where
``bidict`` could be used in the Python standard library instead of having to
maintain the two ``name2codepoint`` and ``codepoint2name`` dictionaries
separately by hand.
Caveats
-------
* Because ``bidict`` is a bidirectional dict, values as well as keys must be
hashable. Attempting to insert an unhashable value will result in an error::
>>> anagrams_by_alphagram = bidict(opt=['opt', 'pot', 'top']) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
TypeError:...unhashable...
>>> bidict(opt=('opt', 'pot', 'top'))
bidict({'opt': ('opt', 'pot', 'top')})
* When instantiating or updating a ``bidict``, remember that mappings for
like values with differing keys will be silently dropped (just as the dict
literal ``{1: 'one', 1: 'uno'}`` silently drops a mapping), to maintain
bidirectionality::
>>> nils = bidict(zero=0, zilch=0, zip=0)
>>> len(nils)
1
>>> nils.update(nix=0, nada=0)
>>> len(nils)
1
* When mapping the key of one existing mapping to the value of another (or
vice versa), the two mappings silently collapse into one::
>>> b = bidict({1: 'one', 2: 'two'})
>>> b[1] = 'two'
>>> b
bidict({1: 'two'})
>>> b = bidict({1: 'one', 2: 'two'})
>>> b[:'two'] = 1
>>> b
bidict({1: 'two'})
Links
-----
* Documentation: https://bidict.readthedocs.org
* Development: https://github.com/jab/bidict
* License: http://choosealicense.com/licenses/isc/
Credits
-------
* Thanks to Terry Reedy for the idea for the slice syntax.
* Thanks to Raymond Hettinger for the idea for namedbidict and pointing out
various caveats.
* Thanks to Francis Carr for the idea of storing the inverse bidict.
See the rest of the bidict module for further documentation.
------------------------------------------------------------
'''
from sys import version_info
PY2 = version_info[0] == 2
if PY2:
assert version_info[1] > 6, 'Python >= 2.7 required'
from collections import Hashable, Iterator, Mapping, MutableMapping
from re import compile
from sys import version_info
if PY2:
iteritems = lambda x: x.iteritems()
viewitems = lambda x: x.viewitems()
else:
iteritems = lambda x: iter(x.items())
viewitems = lambda x: x.items()
def fancy_iteritems(*map_or_it, **kw):
'''
Generator yielding the mappings provided.
Abstracts differences between Python 2 and 3::
>>> it = fancy_iteritems({1: 2})
>>> next(it)
(1, 2)
>>> next(it)
Traceback (most recent call last):
...
StopIteration
Accepts zero or one positional argument which it first tries iterating over
as a mapping (as above), and if that fails, falls back to iterating over as
a sequence, yielding items two at a time::
>>> it = fancy_iteritems([(1, 2), (3, 4)])
>>> next(it)
(1, 2)
>>> next(it)
(3, 4)
>>> next(it)
Traceback (most recent call last):
...
StopIteration
>>> list(fancy_iteritems())
[]
Mappings may also be passed as keyword arguments, which will be yielded
after any mappings passed via positional argument::
>>> list(sorted(fancy_iteritems(a=1, b=2)))
[('a', 1), ('b', 2)]
>>> list(sorted(fancy_iteritems({'a': 1}, b=2, c=3)))
[('a', 1), ('b', 2), ('c', 3)]
>>> list(sorted(fancy_iteritems([('a', 1)], b=2, c=3)))
[('a', 1), ('b', 2), ('c', 3)]
In other words, this is like a generator analog of the dict constructor.
Note that if any mappings from a sequence or keyword argument repeat an
earlier mapping in the positional argument, repeat mappings will still
be yielded, whereas with ``dict`` the last repeat clobbers earlier ones::
>>> dict([('a', 1), ('a', 2)])
{'a': 2}
>>> list(fancy_iteritems([('a', 1), ('a', 2)]))
[('a', 1), ('a', 2)]
>>> dict([('a', 1), ('a', 2)], a=3)
{'a': 3}
>>> list(fancy_iteritems([('a', 1), ('a', 2)], a=3))
[('a', 1), ('a', 2), ('a', 3)]
'''
if map_or_it:
l = len(map_or_it)
if l != 1:
raise TypeError('expected at most 1 argument, got %d' % l)
map_or_it = map_or_it[0]
try:
it = iteritems(map_or_it) # mapping?
except AttributeError: # no
for (k, v) in map_or_it: # -> treat as sequence
yield (k, v)
else: # yes
for (k, v) in it: # -> treat as mapping
yield (k, v)
for (k, v) in iteritems(kw):
yield (k, v)
class inverted(Iterator):
'''
An iterator in the spirit of ``reversed``. Useful for inverting a mapping::
>>> keys = (1, 2, 3)
>>> vals = ('one', 'two', 'three')
>>> fwd = dict(zip(keys, vals))
>>> inv = dict(inverted(fwd))
>>> inv == dict(zip(vals, keys))
True
Passing an iterable of pairs produces an iterable of the pairs' inverses::
>>> seq = [(1, 'one'), (2, 'two'), (3, 'three')]
>>> list(inverted(seq))
[('one', 1), ('two', 2), ('three', 3)]
Passing an ``inverted`` object back into ``inverted`` produces the original
sequence of pairs::
>>> seq == list(inverted(inverted(seq)))
True
Under the covers, ``inverted`` first tries to call ``__inverted__`` on the
wrapped object and returns the result if the call succeeded, creating
synergy with :attr:`bidict.BidirectionalMapping.__inverted__` (effectively
delegating to the class if it supports inverting natively). If the call
fails, ``inverted`` falls back on calling its own ``__next__`` method,
which in turn calls :attr:`bidict.fancy_iteritems` on the wrapped object,
yielding the inverse of the pairs that fancy_iteritems yields.
Be careful with passing the inverse of a non-injective mapping into
``dict``; mappings for like values with differing keys will be dropped
silently, just as ``{1: 'one', 1: 'uno'}`` silently drops a mapping::
>>> squares = {-2: 4, -1: 1, 0: 0, 1: 1, 2: 4}
>>> len(squares)
5
>>> len(dict(inverted(squares)))
3
'''
def __init__(self, data):
self._data = data
def __iter__(self):
try:
it = self._data.__inverted__
except AttributeError:
it = self.__next__
return it()
def __next__(self):
for (k, v) in fancy_iteritems(self._data):
yield (v, k)
# compat
if PY2:
next = __next__
class BidirectionalMapping(Mapping):
'''
The read-only functionality of ``bidict`` is implemented in this base
class. :class:`bidict.bidict` and :class:`bidict.frozenbidict` both extend
this.
'''
def __init__(self, *args, **kw):
self._fwd = {}
self._bwd = {}
for (k, v) in fancy_iteritems(*args, **kw):
self._set(k, v)
inv = object.__new__(self.__class__)
inv._fwd = self._bwd
inv._bwd = self._fwd
inv._inv = self
self._inv = inv
self._hash = None
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self._fwd)
__str__ = __repr__
def __eq__(self, other):
'''
Supports equality testing with another mapping::
>>> d = dict(a=1)
>>> b = bidict(a=1)
>>> b == d
True
>>> f = frozenbidict(a=1)
>>> f == d
True
>>> ElementMap = namedbidict('ElementMap', 'symbol', 'element')
>>> noble_gases = ElementMap(He='helium')
>>> noble_gases == dict(He='helium')
True
Even works with nan::
>>> nan = float('nan')
>>> bidict({nan: 1}) == {nan: 1}
True
Comparing with a non-mapping returns False:
>>> bidict(a=1) == [('a', 1)]
False
'''
try:
return viewitems(self) == viewitems(other)
except:
return False
def __ne__(self, other):
return not self.__eq__(other)
def __invert__(self):
'''
Called when the unary inverse operator (~) is applied.
'''
return self._inv
inv = property(__invert__, doc='Property providing access to the inverse '\
'bidict. Can be chained as in: ``B.inv.inv is B``')
def __inverted__(self):
return iteritems(self._bwd)
@staticmethod
def _fwd_slice(slice):
'''
Raises ``TypeError`` if the given slice does not have either only
its start or only its stop set to a non-None value.
Returns True if only its start is not None and False if only its stop
is not None.
'''
if slice.step is not None or \
(not ((slice.start is None) ^ (slice.stop is None))):
raise TypeError('Slice must specify only either start or stop')
return slice.start is not None
def __getitem__(self, keyorslice):
if isinstance(keyorslice, slice):
# forward lookup (by key): b[key:]
if self._fwd_slice(keyorslice):
return self._fwd[keyorslice.start]
else: # inverse lookup (by val): b[:val]
return self._bwd[keyorslice.stop]
else: # keyorslice is a key: b[key]
return self._fwd[keyorslice]
def _set(self, key, val):
try:
oldkey = self._bwd[val]
except KeyError:
pass
else:
del self._fwd[oldkey]
try:
oldval = self._fwd[key]
except KeyError:
pass
else:
del self._bwd[oldval]
self._fwd[key] = val
self._bwd[val] = key
get = lambda self, k, *args: self._fwd.get(k, *args)
copy = lambda self: self.__class__(self._fwd)
get.__doc__ = dict.get.__doc__
copy.__doc__ = dict.copy.__doc__
__len__ = lambda self: len(self._fwd)
__iter__ = lambda self: iter(self._fwd)
__contains__ = lambda self, x: x in self._fwd
__len__.__doc__ = dict.__len__.__doc__
__iter__.__doc__ = dict.__iter__.__doc__
__contains__.__doc__ = dict.__contains__.__doc__
keys = lambda self: self._fwd.keys()
items = lambda self: self._fwd.items()
keys.__doc__ = dict.keys.__doc__
items.__doc__ = dict.items.__doc__
values = lambda self: self._bwd.keys()
values.__doc__ = \
"D.values() -> a set-like object providing a view on D's values. " \
"Note that because values of a BidirectionalMapping are also keys, " \
"this returns a ``dict_keys`` object rather than a ``dict_values`` " \
"object."
if PY2:
iterkeys = lambda self: self._fwd.iterkeys()
viewkeys = lambda self: self._fwd.viewkeys()
iteritems = lambda self: self._fwd.iteritems()
viewitems = lambda self: self._fwd.viewitems()
itervalues = lambda self: self._bwd.iterkeys()
viewvalues = lambda self: self._bwd.viewkeys()
iterkeys.__doc__ = dict.iterkeys.__doc__
viewkeys.__doc__ = dict.viewkeys.__doc__
iteritems.__doc__ = dict.iteritems.__doc__
viewitems.__doc__ = dict.viewitems.__doc__
itervalues.__doc__ = dict.itervalues.__doc__
viewvalues.__doc__ = values.__doc__.replace('values()', 'viewvalues()')
values.__doc__ = dict.values.__doc__
class frozenbidict(BidirectionalMapping, Hashable):
'''
Extends ``BidirectionalMapping`` and ``Hashable`` to provide an immutable,
hashable bidict. It's immutable simply because it doesn't implement any
mutating methods::
>>> f = frozenbidict()
>>> f.update(H='hydrogen') # doctest: +ELLIPSIS
Traceback (most recent call last):
...
AttributeError...
>>> f['C'] = 'carbon' # doctest: +ELLIPSIS
Traceback (most recent call last):
...
TypeError...
And, unlike ``BidirectionalMapping`` and ``bidict``, it's hashable simply
because it implements ``__hash__``::
>>> b = bidict(H='hydrogen')
>>> h = hash(b) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
TypeError...
>>> f = frozenbidict(b)
>>> hash(f) is not 'an exception'
True
Hashability allows for insertion into sets, mappings, etc. and associated
hashing semantics::
>>> d = {}
>>> d[f] = 'hashable!'
>>> g = frozenbidict(H='hydrogen')
>>> hash(f) == hash(g)
True
>>> f is g
False
To compute a frozenbidict's hash value, a temporary frozenset is
constructed out of the frozenbidict's items, and the hash of the frozenset
is returned. So be aware that computing a frozenbidict's hash is not
a constant-time or -space operation.
To mitigate this, the hash is computed lazily, only when ``__hash__`` is
first called, and is then cached so that future calls take constant time.
'''
def __hash__(self):
if self._hash is None:
self._hash = hash(frozenset(viewitems(self)))
return self._hash
class bidict(BidirectionalMapping, MutableMapping):
'''
Extends :class:`bidict.BidirectionalMapping` to implement the
``MutableMapping`` interface. The API is a superset of the ``dict`` API
minus the ``fromkeys`` method, which doesn't make sense for a bidirectional
mapping because keys *and* values must be unique.
Examples::
>>> keys = (1, 2, 3)
>>> vals = ('one', 'two', 'three')
>>> bi = bidict(zip(keys, vals))
>>> bi == bidict({1: 'one', 2: 'two', 3: 'three'})
True
>>> bidict(inverted(bi)) == bidict(zip(vals, keys))
True
You can use standard subscripting syntax with a key to get or set a forward
mapping::
>>> bi[2]
'two'
>>> bi[2] = 'twain'
>>> bi[2]
'twain'
>>> bi[4]
Traceback (most recent call last):
...
KeyError: 4
Or use a slice with only a ``start``::
>>> bi[2:]
'twain'
>>> bi[0:] = 'naught'
>>> bi[0:]
'naught'
Use a slice with only a ``stop`` to get or set an inverse mapping::
>>> bi[:'one']
1
>>> bi[:'aught'] = 1
>>> bi[:'aught']
1
>>> bi[1]
'aught'
>>> bi[:'one']
Traceback (most recent call last):
...
KeyError: 'one'
Deleting items from the bidict works the same way::
>>> del bi[0]
>>> del bi[2:]
>>> del bi[:'three']
>>> bi
bidict({1: 'aught'})
bidicts maintain references to their inverses via the ``inv`` property,
which can also be used to access or modify them::
>>> bi.inv
bidict({'aught': 1})
>>> bi.inv['aught']
1
>>> bi.inv[:1]
'aught'
>>> bi.inv[:1] = 'one'
>>> bi.inv
bidict({'one': 1})
>>> bi
bidict({1: 'one'})
>>> bi.inv.inv is bi
True
>>> bi.inv.inv.inv is bi.inv
True
A ``bidict``s inverse can also be accessed via the unary ~ operator, by
analogy to the unary bitwise inverse operator::
>>> ~bi
bidict({'one': 1})
>>> ~bi is bi.inv
True
Because ~ binds less tightly than brackets, parentheses are necessary for
something like::
>>> (~bi)['one']
1
bidicts work with ``inverted`` as expected::
>>> biinv = bidict(inverted(bi))
>>> biinv
bidict({'one': 1})
This of course creates a new object (equivalent but not identical)::
>>> biinv == bi.inv
True
>>> biinv is bi.inv
False
Notice that ``__eq__`` has been implemented to make == work as expected.
Inverting the inverse should round-trip::
>>> bi == bidict(inverted(inverted(bi)))
True
Use ``invert`` to invert the mapping in place, in constant time::
>>> bi.invert()
>>> bi
bidict({'one': 1})
The rest of the ``MutableMapping`` interface is supported too::
>>> bi.get('one')
1
>>> bi.get('zero')
>>> bi.get('zero', 0)
0
>>> list(bi.keys())
['one']
>>> list(bi.values())
[1]
>>> list(bi.items())
[('one', 1)]
>>> bi.setdefault('one', 2)
1
>>> bi.setdefault('two', 2)
2
>>> bi.pop('one')
1
>>> bi.popitem()
('two', 2)
>>> bi.inv.setdefault(3, 'three')
'three'
>>> bi
bidict({'three': 3})
>>> len(bi) # calls __len__
1
>>> [key for key in bi] # calls __iter__, returns keys like dict
['three']
>>> 'three' in bi # calls __contains__
True
>>> list(bi.keys())
['three']
>>> list(bi.values())
[3]
>>> bi.update([('four', 4)])
>>> bi.update({'five': 5}, six=6, seven=7)
>>> sorted(bi.items(), key=lambda x: x[1])
[('three', 3), ('four', 4), ('five', 5), ('six', 6), ('seven', 7)]
When instantiating or updating a ``bidict``, remember that mappings for
like values with differing keys will be silently dropped (just as the
literal ``{1: 'one', 1: 'uno'}`` silently drops a mapping), to maintain
bidirectionality::
>>> nils = bidict(zero=0, zilch=0, zip=0)
>>> len(nils)
1
>>> nils.update(nix=0, nada=0)
>>> len(nils)
1
Another caveat: when mapping the key of one existing mapping to the value
of another (or vice versa), the two mappings collapse into one::
>>> b = bidict({1: 'one', 2: 'two'})
>>> b[1] = 'two'
>>> b
bidict({1: 'two'})
>>> b = bidict({1: 'one', 2: 'two'})
>>> b[:'two'] = 1
>>> b
bidict({1: 'two'})
'''
def __del(self, key):
val = self._fwd[key]
del self._fwd[key]
del self._bwd[val]
def __delitem__(self, keyorslice):
if isinstance(keyorslice, slice):
# delete by key: del b[key:]
if self._fwd_slice(keyorslice):
self.__del(keyorslice.start)
else: # delete by value: del b[:val]
self.__del(self._bwd[keyorslice.stop])
else: # keyorslice is a key: del b[key]
self.__del(keyorslice)
def __setitem__(self, keyorslice, keyorval):
if isinstance(keyorslice, slice):
# keyorslice.start is key, keyorval is val: b[key:] = val
if self._fwd_slice(keyorslice):
self._set(keyorslice.start, keyorval)
else: # keyorval is key, keyorslice.stop is val: b[:val] = key
self._set(keyorval, keyorslice.stop)
else: # keyorslice is a key, keyorval is a val: b[key] = val
self._set(keyorslice, keyorval)
def clear(self):
self._fwd.clear()
self._bwd.clear()
def invert(self):
self._fwd, self._bwd = self._bwd, self._fwd
self._inv._fwd, self._inv._bwd = self._inv._bwd, self._inv._fwd
def pop(self, key, *args):
val = self._fwd.pop(key, *args)
del self._bwd[val]
return val
def popitem(self):
if not self._fwd:
raise KeyError
key, val = self._fwd.popitem()
del self._bwd[val]
return key, val
def setdefault(self, key, default=None):
val = self._fwd.setdefault(key, default)
self._bwd[val] = key
return val
def update(self, *args, **kw):
for k, v in fancy_iteritems(*args, **kw):
self[k] = v
_LEGALNAMEPAT = '^[a-zA-Z][a-zA-Z0-9_]*$'
_LEGALNAMERE = compile(_LEGALNAMEPAT)
def _empty_namedbidict(mapname, fwdname, invname):
'''
Create an empty instance of a custom bidict (namedbidict). This method is
used to make ``namedbidict`` instances picklable.
'''
return namedbidict(mapname, fwdname, invname)()
def namedbidict(mapname, fwdname, invname, bidict_type=bidict):
'''
Generate a custom bidict class in the spirit of ``namedtuple`` with
custom attribute-based access to forward and inverse mappings::
>>> ElementMap = namedbidict('ElementMap', 'symbol', 'element')
>>> noble_gases = ElementMap(He='helium')
>>> noble_gases.element_for['He']
'helium'
>>> noble_gases.symbol_for['helium']
'He'
>>> noble_gases.element_for['Ne'] = 'neon'
>>> del noble_gases.symbol_for['helium']
>>> noble_gases
ElementMap({'Ne': 'neon'})
Pass to ``bidict`` to get back a regular ``bidict``::
>>> bidict(noble_gases)
bidict({'Ne': 'neon'})
Comparison works as expected::
>>> noble_gases2 = ElementMap({'Ne': 'neon'})
>>> noble_gases2 == noble_gases
True
>>> noble_gases2 == bidict(noble_gases)
True
>>> noble_gases2 == dict(noble_gases)
True
>>> noble_gases2['Rn'] = 'radon'
>>> noble_gases2 == noble_gases
False
>>> noble_gases2 != noble_gases
True
>>> noble_gases2 != bidict(noble_gases)
True
>>> noble_gases2 != dict(noble_gases)
True
The ``bidict_type`` keyword arg allows overriding the BidirectionalMapping
type used, useful for creating e.g. a namedfrozenbidict::
>>> ElMap = namedbidict('ElMap', 'sym', 'el', bidict_type=frozenbidict)
>>> noble = ElMap(He='helium')
>>> hash(noble) is not 'an exception'
True
>>> noble['C'] = 'carbon' # doctest: +ELLIPSIS
Traceback (most recent call last):
...
TypeError...
'''
for name in mapname, fwdname, invname:
if _LEGALNAMERE.match(name) is None:
raise ValueError('"%s" does not match pattern %s' %
(name, _LEGALNAMEPAT))
for_fwd = invname + '_for'
for_inv = fwdname + '_for'
__dict__ = {for_fwd: property(lambda self: self),
for_inv: bidict_type.inv}
custombidict = type(mapname, (bidict_type,), __dict__)
# support pickling
custombidict.__reduce__ = lambda self: \
(_empty_namedbidict, (mapname, fwdname, invname), self.__dict__)
return custombidict
if __name__ == '__main__':
from doctest import testmod
testmod()