The Dictionary

class RangeDict(self, iterable, *, identity=False)

A class representing a dict-like structure where continuous ranges correspond to certain values. For any item given to lookup, the value obtained from a RangeDict will be the one corresponding to the first range into which the given item fits. Otherwise, RangeDict provides a similar interface to python’s built-in dict.

A RangeDict can be constructed in one of four ways:

>>> # Empty
>>> a = RangeDict()
>>> # From an existing RangeDict object
>>> b = RangeDict(a)
>>> # From a dict that maps Ranges to values
>>> c = RangeDict({
...     Range('a', 'h'): "First third of the lowercase alphabet",
...     Range('h', 'p'): "Second third of the lowercase alphabet",
...     Range('p', '{'): "Final third of the lowercase alphabet",
... })
>>> print(c['brian'])  # First third of the lowercase alphabet
>>> print(c['king arthur'])  # Second third of the lowercase alphabet
>>> print(c['python'])  # Final third of the lowercase alphabet
>>> # From an iterable of 2-tuples, like a regular dict
>>> d = RangeDict([
...     (Range('A', 'H'), "First third of the uppercase alphabet"),
...     (Range('H', 'P'), "Second third of the uppercase alphabet"),
...     (Range('P', '['), "Final third of the uppercase alphabet"),
... ])

A RangeDict cannot be constructed from an arbitrary number of positional arguments or keyword arguments.

RangeDicts are mutable, so new range correspondences can be added at any time, with Ranges or RangeSets acting like the keys in a normal dict/hashtable. New keys must be of type Range or RangeSet, or they must be able to be coerced into a RangeSet. Given keys are also copied before they are added to a RangeDict.

Adding a new range that overlaps with an existing range will make it so that the value returned for any given number will be the one corresponding to the most recently-added range in which it was found (Ranges are compared by start, include_start, end, and include_end in that priority order). Order of insertion is important.

The RangeDict constructor, and the .update() method, insert elements in order from the iterable they came from. As of python 3.7+, dicts retain the insertion order of their arguments, and iterate in that order - this is respected by this data structure. Other iterables, like lists and tuples, have order built-in. Be careful about using sets as arguments, since they have no guaranteed order.

Be very careful about adding a range from -infinity to +infinity. If defined using the normal Range constructor without any start/end arguments, then that Range will by default accept any value (see Range’s documentation for more info). However, the first non-infinite Range added to the RangeDict will overwrite part of the infinite Range, and turn it into a Range of that type only. As a result, other types that the infinite Range may have accepted before, will no longer work:

>>> e = RangeDict({Range(include_end=True): "inquisition"})
>>> print(e)  # {{[-inf, inf)}: inquisition}
>>> print(e.get(None))  # inquisition
>>> print(e.get(3))  # inquisition
>>> print(e.get("holy"))  # inquisition
>>> print(e.get("spanish"))  # inquisition
>>>
>>> e[Range("a", "m")] = "grail"
>>>
>>> print(e)  # {{[-inf, a), [m, inf)}: inquisition, {[a, m)}: grail}
>>> print(e.get("spanish"))  # inquisition
>>> print(e.get("holy"))  # grail
>>> print(e.get(3))  # KeyError
>>> print(e.get(None))  # KeyError

In general, unless something has gone wrong, the RangeDict will not include any empty ranges. Values will disappear if there are not any keys that map to them. Adding an empty Range to the RangeDict will not trigger an error, but will have no effect.

By default, the range set will determine value uniqueness by equality (==), not by identity (is), and multiple rangekeys pointing to the same value will be compressed into a single RangeSet pointed at a single value. This is mainly meaningful for values that are mutable, such as list`s or `set`s. If using assignment operators besides the generic `= (+=, |=, etc.) on such values, be warned that the change will reflect upon the entire rangeset.

>>> # [{3}] == [{3}] is True, so the two ranges are made to point to the same object
>>> f = RangeDict({Range(1, 2): {3}, Range(4, 5): {3}})
>>> print(f)  # {{[1, 2), [4, 5)}: {3}}
>>>
>>> # f[1] returns the {3}. When |= is used, this object changes to {3, 4}
>>> f[Range(1, 2)] |= {4}
>>> # since the entire rangeset is pointing at the same object, the entire range changes
>>> print(f)  # {{[1, 2), [4, 5)}: {3, 4}}

This is because dict[value] = newvalue calls dict.__setitem__(), whereas dict[value] += item instead calls dict[value].__iadd__() instead. To make the RangeDict use identity comparison instead, construct it with the keyword argument identity=True, which should help:

>>> # `{3} is {3}` is False, so the two ranges don't coalesce
>>> g = RangeDict({Range(1, 2): {3}, Range(4, 5): {3}}, identity=True)
>>> print(g)  # {{[1, 2)}: {3}, {[4, 5)}: {3}}

To avoid the problem entirely, you can also simply not mutate mutable values that multiple rangekeys may refer to, substituting non-mutative operations:

>>> h = RangeDict({Range(1, 2): {3}, Range(4, 5): {3}})
>>> print(h)  # {{[1, 2), [4, 5)}: {3}}
>>> h[Range(1, 2)] = h[Range(1, 2)] | {4}
>>> print(h)  # {{[4, 5)}: {3}, {[1, 2)}: {3, 4}}
__init__(iterable, *, identity=False)

Initialize a new RangeDict from the given iterable. The given iterable may be either a RangeDict (in which case, a copy will be created), a regular dict with all keys able to be converted to Ranges, or an iterable of 2-tuples (range, value).

If the argument identity=True is given, the RangeDict will use is instead of == when it compares multiple rangekeys with the same associated value to possibly merge them.

Parameters:
  • iterable – Optionally, an iterable from which to source keys - either a RangeDict, a regular dict with Rangelike objects as keys, or an iterable of (range, value) tuples.

  • identity – optionally, a toggle to use identity instead of equality when determining key-value similarity. By default, uses equality, but will use identity instead if True is passed.