MCPcopy Create free account
hub / github.com/python/mypy / DisjointDict

Class DisjointDict

mypy/checker.py:9315–9407  ·  view source on GitHub ↗

An variation of the union-find algorithm/data structure where instead of keeping track of just disjoint sets, we keep track of disjoint dicts -- keep track of multiple Set[Key] -> Set[Value] mappings, where each mapping's keys are guaranteed to be disjoint. This data structure is curren

Source from the content-addressed store, hash-verified

9313
9314
9315class DisjointDict(Generic[TKey, TValue]):
9316 """An variation of the union-find algorithm/data structure where instead of keeping
9317 track of just disjoint sets, we keep track of disjoint dicts -- keep track of multiple
9318 Set[Key] -> Set[Value] mappings, where each mapping's keys are guaranteed to be disjoint.
9319
9320 This data structure is currently used exclusively by 'group_comparison_operands' below
9321 to merge chains of '==' and 'is' comparisons when two or more chains use the same expression
9322 in best-case O(n), where n is the number of operands.
9323
9324 Specifically, the `add_mapping()` function and `items()` functions will take on average
9325 O(k + v) and O(n) respectively, where k and v are the number of keys and values we're adding
9326 for a given chain. Note that k <= n and v <= n.
9327
9328 We hit these average/best-case scenarios for most user code: e.g. when the user has just
9329 a single chain like 'a == b == c == d == ...' or multiple disjoint chains like
9330 'a==b < c==d < e==f < ...'. (Note that a naive iterative merging would be O(n^2) for
9331 the latter case).
9332
9333 In comparison, this data structure will make 'group_comparison_operands' have a worst-case
9334 runtime of O(n*log(n)): 'add_mapping()' and 'items()' are worst-case O(k*log(n) + v) and
9335 O(k*log(n)) respectively. This happens only in the rare case where the user keeps repeatedly
9336 making disjoint mappings before merging them in a way that persistently dodges the path
9337 compression optimization in '_lookup_root_id', which would end up constructing a single
9338 tree of height log_2(n). This makes root lookups no longer amoritized constant time when we
9339 finally call 'items()'.
9340 """
9341
9342 def __init__(self) -> None:
9343 # Each key maps to a unique ID
9344 self._key_to_id: dict[TKey, int] = {}
9345
9346 # Each id points to the parent id, forming a forest of upwards-pointing trees. If the
9347 # current id already is the root, it points to itself. We gradually flatten these trees
9348 # as we perform root lookups: eventually all nodes point directly to its root.
9349 self._id_to_parent_id: dict[int, int] = {}
9350
9351 # Each root id in turn maps to the set of values.
9352 self._root_id_to_values: dict[int, set[TValue]] = {}
9353
9354 def add_mapping(self, keys: set[TKey], values: set[TValue]) -> None:
9355 """Adds a 'Set[TKey] -> Set[TValue]' mapping. If there already exists a mapping
9356 containing one or more of the given keys, we merge the input mapping with the old one.
9357
9358 Note that the given set of keys must be non-empty -- otherwise, nothing happens.
9359 """
9360 if not keys:
9361 return
9362
9363 subtree_roots = [self._lookup_or_make_root_id(key) for key in keys]
9364 new_root = subtree_roots[0]
9365
9366 root_values = self._root_id_to_values[new_root]
9367 root_values.update(values)
9368 for subtree_root in subtree_roots[1:]:
9369 if subtree_root == new_root or subtree_root not in self._root_id_to_values:
9370 continue
9371 self._id_to_parent_id[subtree_root] = new_root
9372 root_values.update(self._root_id_to_values.pop(subtree_root))

Callers 2

newMethod · 0.90

Calls

no outgoing calls

Tested by 1

newMethod · 0.72

Used in the wild real call sites across dependent graphs

searching dependent graphs…