I have a data structure which essentially amounts to a nested dictionary. Let's say it looks like this:
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36}}}
Now, maintaining and creating this is pretty painful; every time I have a new state/county/profession I have to create the lower layer dictionaries via obnoxious try/catch blocks. Moreover, I have to create annoying nested iterators if I want to go over all the values.
I could also use tuples as keys, like such:
{('new jersey', 'mercer county', 'plumbers'): 3,
('new jersey', 'mercer county', 'programmers'): 81,
('new jersey', 'middlesex county', 'programmers'): 81,
('new jersey', 'middlesex county', 'salesmen'): 62,
('new york', 'queens county', 'plumbers'): 9,
('new york', 'queens county', 'salesmen'): 36}
This makes iterating over the values very simple and natural, but it is more syntactically painful to do things like aggregations and looking at subsets of the dictionary (e.g. if I just want to go state-by-state).
Basically, sometimes I want to think of a nested dictionary as a flat dictionary, and sometimes I want to think of it indeed as a complex hierarchy. I could wrap this all in a class, but it seems like someone might have done this already. Alternatively, it seems like there might be some really elegant syntactical constructions to do this.
How could I do this better?
Addendum: I'm aware of setdefault()
but it doesn't really make for clean syntax. Also, each sub-dictionary you create still needs to have setdefault()
manually set.
Testing:
Output:
You could create a YAML file and read it in using PyYaml.
Step 1: Create a YAML file, "employment.yml":
Step 2: Read it in Python
and now
my_shnazzy_dictionary
has all your values. If you needed to do this on the fly, you can create the YAML as a string and feed that intoyaml.safe_load(...)
.Unless your dataset is going to stay pretty small, you might want to consider using a relational database. It will do exactly what you want: make it easy to add counts, selecting subsets of counts, and even aggregate counts by state, county, occupation, or any combination of these.
Implement
__missing__
on adict
subclass to set and return a new instance.This approach has been available (and documented) since Python 2.5, and (particularly valuable to me) it pretty prints just like a normal dict, instead of the ugly printing of an autovivified defaultdict:
(Note
self[key]
is on the left-hand side of assignment, so there's no recursion here.)and say you have some data:
Here's our usage code:
And now:
Criticism
A criticism of this type of container is that if the user misspells a key, our code could fail silently:
And additionally now we'd have a misspelled county in our data:
Explanation:
We're just providing another nested instance of our class
Vividict
whenever a key is accessed but missing. (Returning the value assignment is useful because it avoids us additionally calling the getter on the dict, and unfortunately, we can't return it as it is being set.)Note, these are the same semantics as the most upvoted answer but in half the lines of code - nosklo's implementation:
Demonstration of Usage
Below is just an example of how this dict could be easily used to create a nested dict structure on the fly. This can quickly create a hierarchical tree structure as deeply as you might want to go.
Which outputs:
And as the last line shows, it pretty prints beautifully and in order for manual inspection. But if you want to visually inspect your data, implementing
__missing__
to set a new instance of its class to the key and return it is a far better solution.Other alternatives, for contrast:
dict.setdefault
Although the asker thinks this isn't clean, I find it preferable to the
Vividict
myself.and now:
A misspelling would fail noisily, and not clutter our data with bad information:
Additionally, I think setdefault works great when used in loops and you don't know what you're going to get for keys, but repetitive usage becomes quite burdensome, and I don't think anyone would want to keep up the following:
Another criticism is that setdefault requires a new instance whether it is used or not. However, Python (or at least CPython) is rather smart about handling unused and unreferenced new instances, for example, it reuses the location in memory:
An auto-vivified defaultdict
This is a neat looking implementation, and usage in a script that you're not inspecting the data on would be as useful as implementing
__missing__
:But if you need to inspect your data, the results of an auto-vivified defaultdict populated with data in the same way looks like this:
This output is quite inelegant, and the results are quite unreadable. The solution typically given is to recursively convert back to a dict for manual inspection. This non-trivial solution is left as an exercise for the reader.
Performance
Finally, let's look at performance. I'm subtracting the costs of instantiation.
Based on performance,
dict.setdefault
works the best. I'd highly recommend it for production code, in cases where you care about execution speed.If you need this for interactive use (in an IPython notebook, perhaps) then performance doesn't really matter - in which case, I'd go with Vividict for readability of the output. Compared to the AutoVivification object (which uses
__getitem__
instead of__missing__
, which was made for this purpose) it is far superior.Conclusion
Implementing
__missing__
on a subclasseddict
to set and return a new instance is slightly more difficult than alternatives but has the benefits ofand because it is less complicated and more performant than modifying
__getitem__
, it should be preferred to that method.Nevertheless, it has drawbacks:
Thus I personally prefer
setdefault
to the other solutions, and have in every situation where I have needed this sort of behavior.defaultdict()
is your friend!For a two dimensional dictionary you can do:
For more dimensions you can:
Example:
Edit: Now returning dictionaries when querying with wild cards (
None
), and single values otherwise.