Dictionary Represented Using Lists

Accessing nested dictionaries often calls for recursive functions in order to properly parse and/or walk through them. This isn’t always easy to code around. ListDict provides an alternative view on nested dictionaries, breaking down the value nested within keys to a simple concept of path and value. This flattens the nesting into a linear list, greatly simplifying the coding around nested dictionaries.

Concept

Consider the following nested dictionary:

# Nested Dict
# -----------

d = {
    'a': {
        'b': {
            'c': {
                'd': 'value',
            },
        },
    },
    'x': {
        'y': {
            'z': 100,
        },
    },
}


# accessing 'value', means
d['a']['b']['c']['d']
# 'value'

# accessing 100, means
d['x']['y']['z']
# 100

Looking at the above code, it’s not hard to generalize the access pattern into path/value, where:

  • d['a']['b']['c']['d'] can be broken down to:

    • Path: ['a', 'b', 'c', 'd']

    • Value: 'value'

  • d['x']['y']['z'] can be broken down to:

    • Path: ['x', 'y', 'z']

    • Value: 100

Where each path value represents a level of dictionary nesting, with the last path key holding the final value at the end of the chain.

Taking advantage of this pattern, ListDict takes each nested dict and breaks it down into a list of (path, value) (DictItem namedtuples):

# Representing Nested Dict using path/value
# -----------------------------------------
#
#   reusing the dictionary 'd' from before

from pyats.datastructures import ListDict

# ListDict format:
#   [(path_x, value_x),
#    (path_y, value_y),
#     ... ]
#
# where path is of the form tuple():
#   (nesting_a, nesting_b, ... , final_key)

ld = ListDict(d)
# [DictItem(path=('x', 'y', 'z'), value=100),
#  DictItem(path=('a', 'b', 'c', 'd'), value='value')]

Each item stored within a ListDict corresponds to a path of nested dicts to a stored end value. Same paths always yield the same dict, for example:

# Understanding Paths
# -------------------
#
#   same paths always yield the same dict

# given path ('a', 'b', 'c') and ('a', 'b', 'e')
# notice that the first two position 'a' and 'b' are similar
# and only the last position 'c' and 'e' is different.

# this suggests the following nested datastructure:
suggest = {
    'a': {
        'b': {
            'c': object(),
            'e': object(),
        },
    },
}

Creation

A ListDict can only be instantiated from another (nested) dict.

# Example
# -------
#
#   Creating ListDict

from pyats.datastructures import ListDict

# reusing 'suggest' variable from previous section
ld = ListDict(suggest)
# [DictItem(path=('a', 'b', 'e'), value=<object object at 0xf7683f40>),
#  DictItem(path=('a', 'b', 'c'), value=<object object at 0xf7683d00>)]

The returned datastructure is simply a list, except that the content of the list is always of format path/value (DictItem named tuple).

Access & Reconstruction

ListDict is an extension (inheriting from) list, and thus all known APIs of list is expected to continue to work.

# Example
# -------
#
#   Accessing ListDict

from pyats.datastructures import ListDict

# reusing 'ld' from above
ld[0]
# DictItem(path=('a', 'b', 'e'), value=<object object at 0xf7683f40>)
ld[0].path
# ('a', 'b', 'e')
ld[0].value
# <object object at 0xf7683f40>

One important property of each ListDict is that it is mutable: the content of each instance can be modified, which means when you flatten out a nested dict, you have the ability to add and/or remove content from it as needed.

# Example
# -------
#
#   modifying and looping ListDict

# continuing from above example...

# appending a new path/value
ld.append((('a', 'b', 'x'), object()))
# [DictItem(path=('a', 'b', 'c'), value=<object object at 0xf7692d00>),
#  DictItem(path=('a', 'b', 'e'), value=<object object at 0xf7692f40>),
#  DictItem(path=('a', 'b', 'x'), value=<object object at 0xf7692ea8>)]

# looping through
for i in ld:
    print(i)
# DictItem(path=('a', 'b', 'c'), value=<object object at 0xf7692d00>)
# DictItem(path=('a', 'b', 'e'), value=<object object at 0xf7692f40>)
# DictItem(path=('a', 'b', 'x'), value=<object object at 0xf7692ea8>)

# etc..

Hint

the whole point of ListDict and breaking information down to path/value is so that users can easily loop through the whole original datastructure and do …stuff… not having to write recursive functions.

At the end, each ListDict object can also be re-constructed from its special path/value format, back to its represented dict format by calling the reconstruct() api.

# Example
# -------
#
#   reconstructing a dict from ListDict

# continuing from above example...
new_dict = ld.reconstruct()
# {'a': {
#     'b': {
#         'x': <object object at 0xf7716ce8>,
#         'c': <object object at 0xf7716f40>,
#         'e': <object object at 0xf7716ea8>}
#     }
# }

# id(new_dict) is not the same as id(suggest)
id(new_dict) == id(suggest)
# False

The creation of a ListDict object and reconstructing a dict object is the easiest way to take nested dict formats, flatten it, operate on it, and return it to original state. However, keep in mind that the process is destructive: the newly created dictionary is a new object.