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)]}}
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 list
s, 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)]}}}
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)]}}}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With