Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create dynamic level nested dict from a list of objects?

Tags:

python

I am trying to turn a list of objects into a nested dict which could be accessed by indexes.

The following code works for a two-level nested dictionary. I would like to extend it to work flexibly for any number of levels.

from collections import namedtuple
import pprint 

Holding = namedtuple('holding', ['portfolio', 'ticker', 'shares'])
lst = [
        Holding('Large Cap', 'TSLA', 100),
        Holding('Large Cap', 'MSFT', 200),
        Holding('Small Cap', 'UTSI', 500)
]

def indexer(lst, indexes):
    """Creates a dynamic nested dictionary based on indexes."""
    result = {}
    for item in lst:
        index0 = getattr(item, indexes[0])
        index1 = getattr(item, indexes[1])
        result.setdefault(index0, {}).setdefault(index1, [])
        result[index0][index1].append(item)
    return result 


d = indexer(lst, ['portfolio', 'ticker'])
pp = pprint.PrettyPrinter()
pp.pprint(d)

Outputs:

{'Large Cap': {'MSFT': [holding(portfolio='Large Cap', ticker='MSFT', shares=200)],
               'TSLA': [holding(portfolio='Large Cap', ticker='TSLA', shares=100)]},
 'Small Cap': {'UTSI': [holding(portfolio='Small Cap', ticker='UTSI', shares=500)]}}
like image 974
ChaimG Avatar asked Dec 26 '17 21:12

ChaimG


2 Answers

One of the best ways I've ever seen to implement nested dictionaries is Aaron Hall's answer to the question What is the best way to implement nested dictionaries?. This is an example of implementing a type that does something called "Autovivification" in the Perl programming language.

Anyway, using one here would be useful because it means you only need to call setdefault() for the "leaves" of your tree-like data structure (which are lists, not sub-dictionaries).

So here's an answer to your question that makes use of it:

from collections import namedtuple
from functools import reduce
from operator import attrgetter
from pprint import pprint


Holding = namedtuple('Holding', ['portfolio', 'ticker', 'shares'])

lst = [Holding('Large Cap', 'TSLA', 100),
       Holding('Large Cap', 'MSFT', 200),
       Holding('Small Cap', 'UTSI', 500),]

def indexer(lst, indexes):
    """ Creates a dynamic nested dictionary based on indexes. """

    class Vividict(dict):
        """ dict subclass which dynamically creates sub-dictionaries when
            they're first referenced (and don't exist).
            See https://stackoverflow.com/a/19829714/355230
        """
        def __missing__(self, key):
            value = self[key] = type(self)()
            return value

    result = Vividict()
    index_getters = attrgetter(*indexes)
    for item in lst:
        *indices, leaf = index_getters(item)  # Leaves are lists, not dicts.
        target = reduce(lambda x, y: x[y], indices, result)
        target.setdefault(leaf, []).append(item)

    return result

d = indexer(lst, ['portfolio', 'ticker'])
pprint(d)
print()
d = indexer(lst, ['portfolio', 'ticker', 'shares'])
pprint(d)

Output:

{'Large Cap': {'MSFT': [Holding(portfolio='Large Cap', ticker='MSFT', shares=200)],
               'TSLA': [Holding(portfolio='Large Cap', ticker='TSLA', shares=100)]},
 'Small Cap': {'UTSI': [Holding(portfolio='Small Cap', ticker='UTSI', shares=500)]}}

{'Large Cap': {'MSFT': {200: [Holding(portfolio='Large Cap', ticker='MSFT', shares=200)]},
               'TSLA': {100: [Holding(portfolio='Large Cap', ticker='TSLA', shares=100)]}},
 'Small Cap': {'UTSI': {500: [Holding(portfolio='Small Cap', ticker='UTSI', shares=500)]}}}
like image 105
martineau Avatar answered Oct 22 '22 20:10

martineau


You could try sth along the following lines. Just iterate the list of attribtes specified by the indexes and keep following down the thus created nested dict:

def indexer(lst, indexes):
    result = {}
    for item in lst:
        attrs = [getattr(item, i) for i in indexes]
        crnt = result  # always the dict at the current nesting level
        for attr in attrs[:-1]:
            # follow one level deeper
            crnt = crnt.setdefault(attr, {})  
        crnt.setdefault(attrs[-1], []).append(item)
    return result 

This produces the following outputs:

>>> d = indexer(lst, ['portfolio', 'ticker'])
{'Large Cap': {'ticker': [holding(portfolio='Large Cap', ticker='TSLA', shares=100),
                          holding(portfolio='Large Cap', ticker='MSFT', shares=200)]},
 'Small Cap': {'ticker': [holding(portfolio='Small Cap', ticker='UTSI', shares=500)]}}

>>> d = indexer(lst, ['portfolio', 'ticker', 'shares'])
{'Large Cap': {'MSFT': {200: [holding(portfolio='Large Cap', ticker='MSFT', shares=200)]},
               'TSLA': {100: [holding(portfolio='Large Cap', ticker='TSLA', shares=100)]}},
 'Small Cap': {'UTSI': {500: [holding(portfolio='Small Cap', ticker='UTSI', shares=500)]}}}
like image 1
user2390182 Avatar answered Oct 22 '22 19:10

user2390182