lkj.dicts#
Tools for working with dictionaries (and other Mappings).
If you are looking for more, check out the lkj.iterables module too
(after all, dicts are iterables).
Functions
|
Compare two dictionaries' values field by field |
|
Returns a new dictionary with only the keys not in |
|
Returns a new dictionary with only the keys in |
|
Merge multiple mappings into a single mapping, recursively if needed, with customizable conflict resolution for non-mapping values. |
|
Returns a new dictionary with the same nested keys structure, where: |
- lkj.dicts.compare_field_values(dict1, dict2, *, field_comparators={}, default_comparator=<built-in function eq>, aggregator=<function <lambda>>, get_comparison_fields=<function _common_keys_list>)[source]#
Compare two dictionaries’ values field by field
- Parameters:
dict1 – The first dictionary.
dict2 – The second dictionary.
field_comparators (
dict[TypeVar(KT),Callable[[dict,dict],Any]]) – A dictionary where keys are field names and values are comparator functions.default_comparator (
Callable[[dict,dict],Any]) – A default comparator function to use if no specific comparator is provided for a field.aggregator (
Callable[[dict[TypeVar(KT),Any]],Any]) – A function to aggregate the comparison results into a final comparison object.
- Returns:
A final score based on the comparison results.
>>> dict1 = {"color": "brown", "animal": "dog"} >>> dict2 = {"color": "brown", "animal": "cat"} >>> dict3 = {"color": "brown", "animal": "bird"} >>> field_comparators = { ... "color": lambda x, y: 1 if x == y else 0, ... "animal": lambda x, y: 1 if len(x) == len(y) else 0 ... } >>> compare_field_values(dict1, dict2, field_comparators=field_comparators) {'color': 1, 'animal': 1} >>> compare_field_values(dict1, dict3, field_comparators=field_comparators) {'color': 1, 'animal': 0} >>> import functools, statistics >>> aggregator = lambda d: statistics.mean(d.values()) >>> mean_of_values = functools.partial( ... compare_field_values, field_comparators=field_comparators, aggregator=aggregator ... ) >>> mean_of_values(dict1, dict2) 1 >>> mean_of_values(dict1, dict3) 0.5
- lkj.dicts.exclusive_subdict(d, exclude)[source]#
Returns a new dictionary with only the keys not in
exclude.Parameters
d (dict): The input dictionary. exclude (set): The set of keys to exclude from the new dictionary.
Example
>>> exclusive_subdict({'a': 1, 'b': 2, 'c': 3}, {'a', 'c'}) {'b': 2}
- lkj.dicts.inclusive_subdict(d, include)[source]#
Returns a new dictionary with only the keys in
include.Parameters
d (dict): The input dictionary. include (set): The set of keys to include in the new dictionary.
Example
>>> assert inclusive_subdict({'a': 1, 'b': 2, 'c': 3}, {'a', 'c'}) == {'a': 1, 'c': 3}
- lkj.dicts.merge_dicts(*mappings, recursive_condition=<function <lambda>>, conflict_resolver=<function <lambda>>, mapping_constructor=<class 'dict'>)[source]#
Merge multiple mappings into a single mapping, recursively if needed, with customizable conflict resolution for non-mapping values.
This function generalizes the normal
dict.update()method, which takes the union of the keys and resolves conflicting values by overriding them with the last value. Whiledict.update()performs a single-level merge,merge_dictsprovides additional flexibility to handle nested mappings. Withmerge_dicts, you can:Control when to recurse (e.g., based on whether a value is a
Mapping).Specify how to resolve value conflicts (e.g., override, add, or accumulate in a list).
Choose the type of mapping (e.g.,
dict,defaultdict) to use as the container.
- Parameters:
mappings (
Mapping[TypeVar(KT),TypeVar(VT)]) – The mappings to merge.recursive_condition (
Callable[[TypeVar(VT)],bool]) – A callable to determine if values should be merged recursively. By default, checks if the value is aMapping.conflict_resolver (
Callable[[TypeVar(VT),TypeVar(VT)],TypeVar(VT)]) – A callable that resolves conflicts between two values. By default, overrides with the last seen value (lambda x, y: y).mapping_constructor (
Callable[[Iterable[tuple[TypeVar(KT),TypeVar(VT)]]],Mapping[TypeVar(KT),TypeVar(VT)]]) – A callable to construct the resulting mapping. Defaults to the standarddictconstructor.
- Return type:
- Returns:
A merged mapping that combines all the input mappings.
Examples
Basic usage with single-level merge (override behavior):
>>> dict1 = {"a": 1} >>> dict2 = {"a": 2, "b": 3} >>> merge_dicts(dict1, dict2) {'a': 2, 'b': 3}
Handling nested mappings with default behavior (override conflicts):
>>> dict1 = {"a": 1, "b": {"x": 10, "y": 20}} >>> dict2 = {"b": {"y": 30, "z": 40}, "c": 3} >>> dict3 = {"b": {"x": 50}, "d": 4} >>> merge_dicts(dict1, dict2, dict3) {'a': 1, 'b': {'x': 50, 'y': 30, 'z': 40}, 'c': 3, 'd': 4}
Resolving conflicts by summing values:
>>> dict1 = {"a": 1} >>> dict2 = {"a": 2} >>> merge_dicts(dict1, dict2, conflict_resolver=lambda x, y: x + y) {'a': 3}
Accumulating conflicting values into a list:
>>> dict1 = {"a": 1, "b": [1, 2]} >>> dict2 = {"b": [3, 4]} >>> merge_dicts(dict1, dict2, conflict_resolver=lambda x, y: x + y if isinstance(x, list) else [x, y]) {'a': 1, 'b': [1, 2, 3, 4]}
Recursing only on specific conditions:
>>> dict1 = {"a": {"nested": 1}} >>> dict2 = {"a": {"nested": 2, "new": 3}} >>> merge_dicts(dict1, dict2) {'a': {'nested': 2, 'new': 3}}
>>> dict1 = {"a": {"nested": [1, 2]}} >>> dict2 = {"a": {"nested": [3, 4]}} >>> merge_dicts(dict1, dict2, recursive_condition=lambda v: isinstance(v, dict)) {'a': {'nested': [3, 4]}}
Using a custom mapping type (
defaultdict):>>> from collections import defaultdict >>> merge_dicts( ... dict1, dict2, mapping_constructor=lambda items: defaultdict(int, items) ... ) defaultdict(<class 'int'>, {'a': defaultdict(<class 'int'>, {'nested': [3, 4]})})
- lkj.dicts.truncate_dict_values(d, *, max_list_size=2, max_string_size=66, middle_marker='...', d_ingress=<function <lambda>>)[source]#
Returns a new dictionary with the same nested keys structure, where:
List values are reduced to a maximum size of max_list_size.
String values longer than max_string_size are truncated in the middle.
Values are preprocessed with d_ingress before type checking.
Parameters
d (dict): The input dictionary. max_list_size (int, optional): Maximum size for lists. Defaults to 2. max_string_size (int, optional): Maximum length for strings. Defaults to None (no truncation). middle_marker (str, optional): String to insert in the middle of truncated strings. Defaults to ‘…’. d_ingress (callable, optional): Function applied to each value before type checking.
Defaults to identity function. Useful for handling special types like pandas Series.
Returns
dict: A new dictionary with truncated lists and strings.
This can be useful when you have a large dictionary that you want to investigate, but printing/logging it takes too much space.
Example
>>> large_dict = {'a': [1, 2, 3, 4, 5], 'b': {'c': [6, 7, 8, 9], 'd': 'A string like this that is too long'}, 'e': [10, 11]} >>> truncate_dict_values(large_dict, max_list_size=3, max_string_size=20) {'a': [1, 2, 3], 'b': {'c': [6, 7, 8], 'd': 'A string...too long'}, 'e': [10, 11]}
You can use
Noneto indicate “no max”:>>> assert ( ... truncate_dict_values(large_dict, max_list_size=None, max_string_size=None) ... == large_dict ... )
For handling special types like pandas Series:
>>> def handle_pandas(obj): ... if hasattr(obj, 'head'): # pandas Series/DataFrame ... return f"<{type(obj).__name__}: {len(obj)} items>" ... return obj >>> # truncate_dict_values(data_with_pandas, d_ingress=handle_pandas)
- Return type: