2. Usage
2.1. Principle
The core concept is straightforward: just as a dictionary can contain another dictionary as a value, a
NestedDictionary naturally extends this idea. In a NestedDictionary, each value that is itself a dictionary
must also be a NestedDictionary. This recursive structure allows for seamless nesting of dictionaries within
dictionaries.
Unlike conventional dictionaries, nested keys in a NestedDictionary are exposed as tuples. This representation
allows for easy access and manipulation of hierarchical data while maintaining compatibility with standard dictionary
operations.
$ a = NestedDictionary({'first': 1,
'second': {'1': "2:1", '2': "2:2", '3': "3:2"},
'third': 3,
'fourth': 4})
a's keys are:
[('first',), ('second', '1'), ('second', '2'), ('second', '3'), ('third',), ('fourth',)]
$ a['second']['1'] = "2:1"
2.2. Initializing and Using Nested Dictionaries
NestedDictionary provides an intuitive interface for working with nested dictionaries, simplifying access and
manipulation of keys at various depth levels. Although NestedDictionary is the public class, the core of the library
lies in the _StackedDict class. This central class is responsible for internal data and attribute management, enabling
NestedDictionary to operate seamlessly.
To initialize a NestedDictionary, you can use syntax similar to that of a classical dictionary.
Here is a basic example:
$ from ndict import NestedDictionary
$ nd = NestedDictionary(
[('first', 1), ('third', 3)],
second={'first': 1, 'second': 2},
indent=2, # Sets the indentation level to 2 spaces for displaying nested structures
strict=True # Enables strict mode for data validation, ensuring compliance with expected rules
)
$
In this example, we create a NestedDictionary with key-value pairs at the top level and a nested dictionary under
the key ‘second’. The indent parameter defines the number of spaces used for indentation at each level of nesting,
making the output more readable. The strict parameter, when set to True, enforces stricter rules for data insertion,
helping to maintain data integrity. When strict is set to False, the NestedDictionary operates in a more lenient
mode, allowing for greater flexibility in data insertion but potentially at the cost of reduced validation and error
checking.
Note
Introduced in version 0.8.0, _StackedDict manages specific attributes. This evolution generalizes the handling
of attributes specific to nested dictionary classes, providing greater flexibility and advanced features for users.
_StackedDict handles the genericity of attributes to propagate in the default_setup attribute. It manages two
of its own attributes, indent and default_factory, but any subclass can define as many as needed
(see below the section for developers).
2.3. Understanding Paths in Nested Dictionaries
In the context of nested dictionaries, a path is a sequence of keys that navigates through the hierarchical structure to access a specific value or sub-dictionary. Think of it as a trail of keys that leads you from the outermost dictionary to a particular point within the nested structure.
For example, consider the following nested dictionary:
data = {
'a': {
'b': {
'c': 1
},
'd': 2
},
'e': 3
}
In this dictionary:
The path
['a', 'b', 'c']leads to the value1.The path
['a', 'b']leads to the dictionary{'c': 1}.The path
['a', 'd']leads to the value2.The path
['e']leads to the value3.
By representing these paths as lists, we can easily describe and manipulate the hierarchical relationships within the dictionary. This concept is particularly useful when working with complex nested structures, as it provides a clear and concise way to reference specific elements.
2.4. Justification for Using Lists to Describe Paths
While standard dictionaries in Python use immutable types such as strings, numbers, or tuples as keys, representing hierarchical paths with lists offers several advantages in the context of nested dictionaries:
Sequential Representation: Lists naturally represent sequences, making them ideal for capturing the order of keys that lead to a specific value in a nested structure. This sequential nature aligns well with the concept of navigating through layers of dictionaries.
Flexibility: Lists are mutable, allowing for dynamic manipulation of paths. This flexibility is beneficial when working with complex or evolving data structures, where paths may need to be extended, modified, or truncated.
Readability: Using lists to represent paths enhances code readability. It provides a clear and intuitive way to understand the hierarchical relationships within the data, making the code easier to maintain and debug.
Compatibility with Recursive Operations: Lists are well-suited for recursive operations, which are common when traversing nested dictionaries. They can be easily passed to and modified within recursive functions, simplifying the implementation of algorithms that operate on hierarchical data.
Consistency with Existing Tools: Many existing tools and libraries that deal with hierarchical data structures, such as JSON or XML parsers, use lists or similar structures to represent paths. By adopting this convention, we maintain consistency with established practices.
2.5. Introducing DictPaths
To manage and access these paths efficiently, we provide the DictPaths class. This class offers a view object that
provides a dictionary-like interface for accessing hierarchical keys as lists. Similar to dict_keys, but tailored
for hierarchical paths in a _StackedDict, DictPaths allows you to:
Iterate over all hierarchical paths in the
_StackedDictas lists.Check if a specific hierarchical path exists within the
_StackedDict.Retrieve the number of hierarchical paths present in the
_StackedDict.
By using DictPaths, you can easily navigate and manipulate complex nested dictionary structures, making your code
more readable and maintainable.
2.6. Behavior
Nested dictionaries inherit from defaultdict. The default_factory attribute characterizes the behavior of this class:
If the nested dictionary is to behave strictly like a dictionary, then the default_factory attribute is set to None.
If you request the value of a key that doesn’t exist, you’ll get a KeyError. The configuration parameter is
strict=True
>>> from ndict_tools import NestedDictionary
>>> nd = NestedDictionary({'first': 1,
'second': {'1': "2:1", '2': "2:2", '3': "3:2"},
'third': 3,
'fourth': 4},
strict=True)
nd.default_factory
>>> nd['fifth']
Traceback (most recent call last):
File "/snap/pycharm-professional/401/plugins/python/helpers/pydev/pydevconsole.py", line 364, in runcode
coro = func()
File "<input>", line 1, in <module>
KeyError: 'fifth'
If the nested dictionary is to have flexible behavior, then the default_factory attribute is set to NestedDictionary.
If you request a key that doesn’t exist, a NestedDictionary instance will be created accordingly and returned. The
configuration parameter is strict=False or no parameter
>>> from ndict_tools import NestedDictionary
>>> nd = NestedDictionary({'first': 1,
'second': {'1': "2:1", '2': "2:2", '3': "3:2"},
'third': 3,
'fourth': 4},
strict=False)
>>> nd.default_factory
<class 'ndict_tools.core.NestedDictionary'>
>>> nd['fifth']
NestedDictionary(<class 'ndict_tools.core.NestedDictionary'>, {})
And with no parameter
>>> from ndict_tools import NestedDictionary
>>> nd = NestedDictionary({'first': 1,
'second': {'1': "2:1", '2': "2:2", '3': "3:2"},
'third': 3,
'fourth': 4})
>>> nd.default_factory
<class 'ndict_tools.core.NestedDictionary'>
>>> nd['fifth']
NestedDictionary(<class 'ndict_tools.core.NestedDictionary'>, {})
2.7. Examples
$ a = NestedDictionary({'first': 1,
'second': {'1': "2:1", '2': "2:2", '3': "3:2"},
'third': 3,
'fourth': 4})
$ b = NestedDictionary(zip(['first', 'second', 'third', 'fourth'],
[1, {'1': "2:1", '2': "2:2", '3': "3:2"}, 3, 4]))
$ c = NestedDictionary([('first', 1),
('second', {'1': "2:1", '2': "2:2", '3': "3:2"}),
('third', 3),
('fourth', 4)])
$ d = NestedDictionary([('third', 3),
('first', 1),
('second', {'1': "2:1", '2': "2:2", '3': "3:2"}),
('fourth', 4)])
$ e = NestedDictionary([('first', 1), ('fourth', 4)],
third = 3,
second = {'1': "2:1", '2': "2:2", '3': "3:2"})
a == b == c == d == e
3. For Developers
The core class of the ndict_tools package is the internal class _StackedDict within the module. This class
orchestrates all tasks related to the nesting of dictionaries.
This class can be extended for other uses by adhering to the following rules:
R1: Instance attributes must be initialized in the
__init__function of the new class.R2: Instance attributes to be propagated should be characterized in the
default_setupparameter.R3: The management of class attribute parameterization must be performed before calling the
__init__function of the parent class.
Here is an example from test evaluations :
from ndict_tools.tools import _StackedDict
class BDict(_StackedDict):
def __init__(self, *args, **kwargs):
# initialize proper attributes
self.balanced = False
# manage default_setup settings parameters
settings = kwargs.pop("default_setup", {})
settings["indent"] = 4
settings["default_factory"] = None
settings["balanced"] = True
# call __init__
super().__init__(*args, **kwargs, default_setup=settings)