Extending bidict
¶
Although bidict
provides the various bidirectional mapping types covered already,
it’s possible that some use case might require something more than what’s provided.
For this reason,
bidict
was written with extensibility in mind.
Let’s look at some examples.
YoloBidict Recipe¶
If you’d like
ON_DUP_DROP_OLD
to be the default on_dup
behavior
(for __init__()
,
__setitem__()
, and
update()
),
you can use the following recipe:
>>> from bidict import bidict, ON_DUP_DROP_OLD
>>> class YoloBidict(bidict):
... __slots__ = ()
... on_dup = ON_DUP_DROP_OLD
>>> b = YoloBidict({'one': 1})
>>> b['two'] = 1 # succeeds, no ValueDuplicationError
>>> b
YoloBidict({'two': 1})
>>> b.update({'three': 1}) # ditto
>>> b
YoloBidict({'three': 1})
Of course, YoloBidict
’s inherited
put()
and
putall()
methods
still allow specifying a custom OnDup
per call via the on_dup argument,
and will both still default to raising for all duplication types.
Further demonstrating bidict
’s extensibility,
to make an OrderedYoloBidict
,
simply have the subclass above inherit from
bidict.OrderedBidict
rather than bidict.bidict
.
Beware of ON_DUP_DROP_OLD
¶
There’s a good reason that bidict
does not provide a YoloBidict
out of the box.
Before you decide to use a YoloBidict
in your own code,
beware of the following potentially unexpected, dangerous behavior:
>>> b = YoloBidict({'one': 1, 'two': 2}) # contains two items
>>> b['one'] = 2 # update one of the items
>>> b # now only has one item!
YoloBidict({'one': 2})
As covered in Key and Value Duplication, setting an existing key to the value of a different existing item causes both existing items to quietly collapse into a single new item.
A safer example of this type of customization would be something like:
>>> from bidict import ON_DUP_RAISE
>>> class YodoBidict(bidict):
... __slots__ = ()
... on_dup = ON_DUP_RAISE
>>> b = YodoBidict({'one': 1})
>>> b['one'] = 2 # Works with a regular bidict, but Yodo plays it safe.
Traceback (most recent call last):
...
KeyDuplicationError: one
>>> b
YodoBidict({'one': 1})
>>> b.forceput('one', 2) # Any destructive change requires more force.
>>> b
YodoBidict({'one': 2})
Sorted Bidict Recipes¶
Suppose you need a bidict that maintains its items in sorted order.
The Python standard library does not include any sorted dict types,
but the excellent
sortedcontainers and
sortedcollections
libraries do.
Armed with these along with bidict’s
_fwdm_cls
and
_invm_cls
attributes,
creating a sorted bidict type is dead simple:
>>> # As an optimization, bidict.bidict includes a mixin class that
>>> # we can't use here (namely bidict._delegating_mixins._DelegateKeysAndItemsToFwdm),
>>> # so extend the parent class, bidict.MutableBidict, instead.
>>> from bidict import MutableBidict
>>> import sortedcontainers
>>> class SortedBidict(MutableBidict):
... """A sorted bidict whose forward items stay sorted by their keys,
... and whose inverse items stay sorted by *their* keys.
... Note: As a result, an instance and its inverse yield their items
... in different orders.
... """
... __slots__ = ()
... _fwdm_cls = sortedcontainers.SortedDict
... _invm_cls = sortedcontainers.SortedDict
... _repr_delegate = list
>>> b = SortedBidict({'Tokyo': 'Japan', 'Cairo': 'Egypt'})
>>> b
SortedBidict([('Cairo', 'Egypt'), ('Tokyo', 'Japan')])
>>> b['Lima'] = 'Peru'
>>> # b stays sorted by its keys:
>>> list(b.items())
[('Cairo', 'Egypt'), ('Lima', 'Peru'), ('Tokyo', 'Japan')]
>>> # b.inverse stays sorted by *its* keys (b's values)
>>> list(b.inverse.items())
[('Egypt', 'Cairo'), ('Japan', 'Tokyo'), ('Peru', 'Lima')]
Here’s a recipe for a sorted bidict whose forward items stay sorted by their keys, and whose inverse items stay sorted by their values. i.e. An instance and its inverse will yield their items in the same order:
>>> import sortedcollections
>>> class KeySortedBidict(MutableBidict):
... __slots__ = ()
... _fwdm_cls = sortedcontainers.SortedDict
... _invm_cls = sortedcollections.ValueSortedDict
... _repr_delegate = list
>>> element_by_atomic_number = KeySortedBidict({
... 3: 'lithium', 1: 'hydrogen', 2: 'helium'})
>>> # stays sorted by key:
>>> element_by_atomic_number
KeySortedBidict([(1, 'hydrogen'), (2, 'helium'), (3, 'lithium')])
>>> # .inverse stays sorted by value:
>>> list(element_by_atomic_number.inverse.items())
[('hydrogen', 1), ('helium', 2), ('lithium', 3)]
>>> element_by_atomic_number[4] = 'beryllium'
>>> list(element_by_atomic_number.inverse.items())
[('hydrogen', 1), ('helium', 2), ('lithium', 3), ('beryllium', 4)]
>>> # This works because a bidict whose _fwdm_cls differs from its _invm_cls computes
>>> # its inverse class -- which (note) is not actually the same class as the original,
>>> # as it needs to have its _fwdm_cls and _invm_cls swapped -- automatically.
>>> # You can see this if you inspect the inverse bidict:
>>> element_by_atomic_number.inverse # Note the different class, which was auto-generated:
KeySortedBidictInv([('hydrogen', 1), ('helium', 2), ('lithium', 3), ('beryllium', 4)])
>>> ValueSortedBidict = element_by_atomic_number.inverse.__class__
>>> ValueSortedBidict._fwdm_cls
<class 'sortedcollections.recipes.ValueSortedDict'>
>>> ValueSortedBidict._invm_cls
<class 'sortedcontainers.sorteddict.SortedDict'>
>>> # Round trips work as expected:
>>> atomic_number_by_element = ValueSortedBidict(element_by_atomic_number.inverse)
>>> atomic_number_by_element
KeySortedBidictInv([('hydrogen', 1), ('helium', 2), ('lithium', 3), ('beryllium', 4)])
>>> KeySortedBidict(atomic_number_by_element.inverse) == element_by_atomic_number
True
>>> # One other useful trick:
>>> # To pass method calls through to the _fwdm SortedDict when not present
>>> # on the bidict instance, provide a custom __getattribute__ method:
>>> def __getattribute__(self, name):
... try:
... return object.__getattribute__(self, name)
... except AttributeError as e:
... return getattr(self._fwdm, name)
>>> KeySortedBidict.__getattribute__ = __getattribute__
>>> # bidict has no .peekitem attr, so the call is passed through to _fwdm:
>>> element_by_atomic_number.peekitem()
(4, 'beryllium')
>>> element_by_atomic_number.inverse.peekitem()
('beryllium', 4)
Next proceed to Other Functionality.