bidict/docs/extending.rst

296 lines
9.1 KiB
ReStructuredText

Extending ``bidict``
--------------------
Although :mod:`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,
:mod:`bidict` was written with extensibility in mind.
Let's look at some examples.
``YoloBidict`` Recipe
#####################
If you'd like
:attr:`~bidict.ON_DUP_DROP_OLD`
to be the default :class:`~bidict.bidict.on_dup` behavior
(for :meth:`~bidict.bidict.__init__`,
:meth:`~bidict.bidict.__setitem__`, and
:meth:`~bidict.bidict.update`),
you can use the following recipe:
.. doctest::
>>> from bidict import bidict, ON_DUP_DROP_OLD
>>> class YoloBidict(bidict):
... 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
:meth:`~bidict.bidict.put` and
:meth:`~bidict.bidict.putall` methods
still allow specifying a custom :class:`~bidict.OnDup`
per call via the *on_dup* argument,
and will both still default to raising for all duplication types.
Further demonstrating :mod:`bidict`'s extensibility,
to make an ``OrderedYoloBidict``,
simply have the subclass above inherit from
:class:`bidict.OrderedBidict`
rather than :class:`bidict.bidict`.
Beware of ``ON_DUP_DROP_OLD``
:::::::::::::::::::::::::::::
There's a good reason that :mod:`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:
.. doctest::
>>> 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 :ref:`basic-usage: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:
.. doctest::
>>> from bidict import ON_DUP_RAISE
>>> class YodoBidict(bidict): # Note, "Yodo" with a "d"
... 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):
...
bidict.KeyDuplicationError: one
>>> b
YodoBidict({'one': 1})
>>> b.forceput('one', 2) # Any destructive change requires more force.
>>> b
YodoBidict({'one': 2})
``WeakrefBidict`` Recipe
########################
Suppose you need a custom bidict type that only retains weakrefs
to some objects whose refcounts you're trying not increment.
With :class:`~bidict.BidictBase`\'s
:attr:`~bidict.BidictBase._fwdm_cls` (forward mapping class) and
:attr:`~bidict.BidictBase._invm_cls` (inverse mapping class) attributes,
accomplishing this is as simple as:
.. doctest::
>>> from bidict import MutableBidict
>>> from weakref import WeakKeyDictionary, WeakValueDictionary
>>> class WeakrefBidict(MutableBidict):
... _fwdm_cls = WeakKeyDictionary
... _invm_cls = WeakValueDictionary
Now you can insert items into *WeakrefBidict* without incrementing any refcounts:
.. doctest::
>>> id_by_obj = WeakrefBidict()
>>> class MyObj:
... def __init__(self, id):
... self.id = id
... def __repr__(self):
... return f'<MyObj id={self.id}>'
>>> o1, o2 = MyObj(1), MyObj(2)
>>> id_by_obj[o1] = o1.id
>>> id_by_obj[o2] = o2.id
>>> id_by_obj
WeakrefBidict({<MyObj id=1>: 1, <MyObj id=2>: 2})
>>> id_by_obj.inverse
WeakrefBidictInv({1: <MyObj id=1>, 2: <MyObj id=2>})
If you drop your references to your objects,
you can see that they get deallocated on CPython right away,
since your *WeakrefBidict* isn't holding on to them:
.. doctest::
:skipif: not_cpython
>>> del o1, o2
>>> id_by_obj
WeakrefBidict()
``SortedBidict`` 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 <http://www.grantjenks.com/docs/sortedcontainers/>`__ and
`sortedcollections <http://www.grantjenks.com/docs/sortedcollections/>`__
libraries do.
Armed with these, along with :class:`~bidict.BidictBase`'s
:attr:`~bidict.BidictBase._fwdm_cls` (forward mapping class) and
:attr:`~bidict.BidictBase._invm_cls` (inverse mapping class) attributes,
creating a sorted bidict is simple:
.. doctest::
>>> from sortedcontainers import SortedDict
>>> 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.
... """
... _fwdm_cls = SortedDict
... _invm_cls = SortedDict
... _repr_delegate = list # only used for list-style repr
>>> b = SortedBidict({'Tokyo': 'Japan', 'Cairo': 'Egypt'})
>>> b
SortedBidict([('Cairo', 'Egypt'), ('Tokyo', 'Japan')])
>>> b['Lima'] = 'Peru'
>>> list(b.items()) # stays sorted by key
[('Cairo', 'Egypt'), ('Lima', 'Peru'), ('Tokyo', 'Japan')]
>>> list(b.inverse.items()) # .inverse stays sorted by *its* keys (b's values)
[('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:
.. doctest::
>>> from sortedcollections import ValueSortedDict
>>> class KeySortedBidict(MutableBidict):
... _fwdm_cls = SortedDict
... _invm_cls = ValueSortedDict
... _repr_delegate = list
>>> elem_by_atomicnum = KeySortedBidict({
... 6: 'carbon', 1: 'hydrogen', 2: 'helium'})
>>> list(elem_by_atomicnum.items()) # stays sorted by key
[(1, 'hydrogen'), (2, 'helium'), (6, 'carbon')]
>>> list(elem_by_atomicnum.inverse.items()) # .inverse stays sorted by value
[('hydrogen', 1), ('helium', 2), ('carbon', 6)]
>>> elem_by_atomicnum[4] = 'beryllium'
>>> list(elem_by_atomicnum.inverse.items())
[('hydrogen', 1), ('helium', 2), ('beryllium', 4), ('carbon', 6)]
Automatic "Get Attribute" Pass-Through
######################################
Python makes it easy to customize a class's "get attribute" behavior.
You can take advantage of this to pass attribute access
through to the backing ``_fwdm`` mapping, for example,
when an attribute is not provided by the bidict class itself:
>>> def __getattribute__(self, name):
... try:
... return object.__getattribute__(self, name)
... except AttributeError:
... return getattr(self._fwdm, name)
>>> KeySortedBidict.__getattribute__ = __getattribute__
Now, even though this ``KeySortedBidict`` itself provides no ``peekitem`` attribute,
the following call still succeeds
because it's passed through to the backing ``SortedDict``:
>>> elem_by_atomicnum.peekitem()
(6, 'carbon')
Dynamic Inverse Class Generation
################################
When a bidict class's
:attr:`~bidict.BidictBase._fwdm_cls` and
:attr:`~bidict.BidictBase._invm_cls`
are the same,
the bidict class is its own inverse class.
(This is the case for all the
:ref:`bidict classes <other-bidict-types:Bidict Types Diagram>`
that come with :mod:`bidict`.)
However, when a bidict's
:attr:`~bidict.BidictBase._fwdm_cls` and
:attr:`~bidict.BidictBase._invm_cls` differ,
as in the ``KeySortedBidict`` and ``WeakrefBidict`` recipes above,
the inverse class of the bidict
needs to have its
:attr:`~bidict.BidictBase._fwdm_cls` and
:attr:`~bidict.BidictBase._invm_cls` swapped.
:class:`~bidict.BidictBase` detects this
and dynamically computes the correct inverse class for you automatically.
You can see this if you inspect ``KeySortedBidict``'s inverse bidict:
>>> elem_by_atomicnum.inverse.__class__.__name__
'KeySortedBidictInv'
Notice that :class:`~bidict.BidictBase` automatically created a
``KeySortedBidictInv`` class and used it for the inverse bidict.
As expected, ``KeySortedBidictInv``'s
:attr:`~bidict.BidictBase._fwdm_cls` and
:attr:`~bidict.BidictBase._invm_cls`
are the opposite of ``KeySortedBidict``'s:
>>> elem_by_atomicnum.inverse._fwdm_cls.__name__
'ValueSortedDict'
>>> elem_by_atomicnum.inverse._invm_cls.__name__
'SortedDict'
:class:`~bidict.BidictBase` also ensures that round trips work as expected:
>>> KeySortedBidictInv = elem_by_atomicnum.inverse.__class__ # i.e. a value-sorted bidict
>>> atomicnum_by_elem = KeySortedBidictInv(elem_by_atomicnum.inverse)
>>> atomicnum_by_elem
KeySortedBidictInv([('hydrogen', 1), ('helium', 2), ('beryllium', 4), ('carbon', 6)])
>>> KeySortedBidict(atomicnum_by_elem.inverse) == elem_by_atomicnum
True
-----
This all goes to show how simple it can be
to compose your own bidirectional mapping types
out of the building blocks that :mod:`bidict` provides.