bidict

repository·main·Indexed 23 days ago

https://github.com/jab/bidict

A bidirectional mapping library for Python that allows for efficient two-way lookups between keys and values. It provides the `bidict` class for mutable mappings, `frozenbidict` for immutable and hashable mappings, and `OrderedBidict` for mappings that maintain insertion order. The library includes tools for handling duplicate keys or values via `OnDup` actions and provides abstract base classes `BidirectionalMapping` and `MutableBidirectionalMapping` for implementing custom bidirectional types.

Tokens
10.3K
Snippets
26
Records
66
Agent score
75%

What's inside bidict

  1. Check for mapping types using collections.abc

    main

    When writing code that needs to check if an object is a dictionary-like mapping or a bidirectional mapping, do not use isinstance(obj, dict). Instead, use the Abstract Base Classes (ABCs) from collections.abc to ensure compatibility with all mapping types (including bidict and ChainMap).

    • Use collections.abc.Mapping to check if an object is a read-only mapping.
    • Use collections.abc.MutableMapping to check if an object is a mutable mapping.
    • Use bidict.BidirectionalMapping to check if an object is a bidirectional mapping.
  2. Making an immutable type hashable

    main

    To make an immutable type hashable (allowing it to be used in dicts and sets), you must implement __hash__ such that the following invariant holds: a == b ⇒ hash(a) == hash(b).

    In bidict.frozenbidict, equality is order-insensitive. Therefore, all contained items must participate in the hash order-insensitively. frozenbidict achieves this by calling ItemsView(self)._hash(), which uses the same algorithm as frozenset.

  3. Understand the bidict class hierarchy

    main

    The bidict library provides several variants of bidirectional mappings, each implementing different Python collection interfaces as appropriate:

    • bidict.bidict: The standard mutable bidirectional mapping. Implements collections.abc.Mapping and collections.abc.MutableMapping.
    • bidict.frozenbidict: An immutable variant. Implements collections.abc.Mapping and collections.abc.Hashable.
    • bidict.OrderedBidict: An ordered variant. Implements collections.abc.MutableMapping and collections.abc.Hashable.

    These classes are built using a modular structure to maximize reuse and performance, starting from base classes in _base.py and specializing through _bidict.py or _orderedbidict.py.

  4. Requirements for bidict values: Hashability and Uniqueness

    main

    Because bidict allows looking up keys by value, two constraints apply to the values used in the mapping:

    1. Values must be hashable: Attempting to use an unhashable type (like a list) as a value will raise a TypeError. Use immutable types like tuple or frozenset instead.
    2. Values must be unique: In a bidirectional map, every value must be unique. Attempting to insert a value that already exists in the mapping will raise a bidict.ValueDuplicationError by default.
    >>> # Unhashable value error
    >>> bidict(opt=['opt', 'pot', 'top'])
    Traceback (most recent call last):
    ...
    TypeError: ...
    
    >>> # Correct way using tuples
    >>> bidict(opt=('opt', 'pot', 'top'))
    bidict({'opt': ('opt', 'pot', 'top')})
    
    >>> # Value duplication error
    >>> b = bidict({'one': 1})
    >>> b['two'] = 1
    Traceback (most recent call last):
    ...
    bidict.ValueDuplicationError: 1
  5. Integrating with Python's collections.abc

    main

    When designing APIs that integrate with Python's collections module, consider the following:

    • Beyond Mapping: bidict implements collections.abc.Mapping as well as additional APIs found in dict and collections.OrderedDict (e.g., setdefault(), popitem()).
    • Virtual Subclassing: By implementing __hash__(), a class becomes a virtual subclass of collections.abc.Hashable via __subclasshook__ without needing to explicitly inherit from it.
    • Custom ABCs: To create your own open Abstract Base Class (ABC), override abc.ABCMeta.__subclasshook__ to check for the required interface.
    • Explicit Registration: Since collections.abc.Mapping and collections.abc.MutableMapping do not implement __subclasshook__, you must either explicitly subclass them to inherit concrete methods or use abc.ABCMeta.register to register as a virtual subclass.
  6. Access the inverse mapping in bidict

    main

    A bidict maintains a bidirectional relationship between keys and values. You can access the inverse mapping (where values become keys and keys become values) using the .inverse attribute or the .inv shortcut. The inverse is automatically kept in sync with the primary mapping and is a constant-time operation.

    Modifying the .inverse mapping directly will also update the primary mapping.

    >>> element_by_symbol = bidict(H='hydrogen')
    >>> element_by_symbol.inverse
    bidict({'hydrogen': 'H'})
    >>> element_by_symbol.inverse['helium'] = 'He'
    >>> element_by_symbol
    bidict({'H': 'hydrogen', 'He': 'helium'})
    >>> element_by_symbol.inv
    bidict({'helium': 'He'})
  7. How bidict works: Bidirectional Mappings

    main

    A bidict is a bidirectional mapping that allows you to look up values by keys and keys by values.

    • Forward Lookup: Use the standard dictionary syntax mapping[key] to retrieve the associated value.
    • Inverse Lookup: Use the .inverse property to access a view of the mapping where the roles of keys and values are swapped. This allows you to perform mapping.inverse[value] to retrieve the original key.
  8. How dynamic inverse class generation works

    main

    When you subclass bidict and provide different classes for _fwdm_cls and _invm_cls, BidictBase automatically handles the inverse mapping.

    It dynamically computes an inverse class (e.g., YourClassNameInv) where the _fwdm_cls and _invm_cls are swapped. This ensures that the .inverse property of your custom bidict behaves correctly and that round-tripping (creating a new bidict from an inverse) works as expected.

  9. Avoid reference cycles with bidict

    main

    A bidict and its .inverse property do not create a strong reference cycle. Internally, bidict uses weakref.ref to store the inverse reference.

    In CPython, this means that when you no longer retain any references to a bidict, its memory will be reclaimed immediately via reference counting.

    Note: In PyPy, memory is reclaimed only when the garbage collector (GC) runs, as PyPy does not use reference counting.

    >>> fwd = bidict(one=1)
    >>> inv = fwd.inverse
    >>> inv.inverse is fwd
    True
  10. Inheriting __hash__ in subclasses

    main

    In Python, if a class implements __hash__(), its subclasses will not automatically inherit it. Python implicitly sets __hash__ = None in classes that do not explicitly define it.

    If you want a subclass to inherit a base class's __hash__ implementation, you must manually assign it in the class body:

    class SubClass(BaseClass):
        __hash__ = BaseClass.__hash__

    This behavior is consistent with how object works: object implements __hash__, but subclasses of object that override __eq__ are not hashable by default.

  11. Understand bidict terminology: inverse vs reverse

    main

    The library uses the term inverse rather than reverse.

    • Reverse: Reversing a collection (like a list) changes the order of elements but keeps the pairs intact.
    • Inverse: Replacing every (k, v) pair with (v, k). This does not require the collection to be ordered and does not guarantee any specific ordering in the result.

    Additionally, while bidict uses the terms keys and values for familiarity, technically the values also act as keys in the inverse mapping. This allows bidict.values() to return a set-like dict_keys object.

  12. How the .inverse attribute works

    main

    The .inverse attribute on a bidict object returns a bidict representing the inverse mapping. It is not just a single lookup mechanism, but a full bidirectional mapping object that allows you to traverse the associations in reverse. Any changes made to the original bidict are immediately reflected in the .inverse view.

    >>> element_by_symbol = bidict({'H': 'hydrogen'})
    >>> element_by_symbol.inverse
    bidict({'hydrogen': 'H'})
    
    >>> element_by_symbol['H'] = 'hydrogène'
    >>> element_by_symbol.inverse
    bidict({'hydrogène': 'H'})