This package provides a simple implementation of an auto-vivified Python dict, i.e., a
dictionary where accessing a missing key doesn't raise the standard KeyError
exception but instead implicitly creates and returns an empty auto-vivified
dict under that key.
- Auto-vivified
VivoDictclass derived from the standard Pythondictclass (no third-party dependencies). - Auto-vivification of arbitrarily-nested
dictobjects. - Convenience methods for
flatten,replace, andapplyoperations. - Free software: MIT license
- Documentation: https://vivodict.readthedocs.io.
My primary motivation for developing this package is because it contained a piece of code I kept copy-pasting like a bloody caveman between projects.
My typical use-cases for this code include:
- Wrap the decoded JSON
dictfrom crummy APIs without a schema that just decide to drop keys for which the values arenullresulting in code with nestedif "key" in result:. This allowed me to either retrieve the value if it was there or at least arriving at an emptydictwhich evaluates toFalsewhen mapping their half-formed data to my own data-structures. - Create arbitrarily-nested dictionaries of code stats that I can keep organized
as I like while using in the code and then quickly
flattento a Graphite compatible format prior to posting them to ... well Graphite.
This would be the typical Python dict behaviour when accessing a missing
key:
>>> d = {"a": 1, "b": 2}
>>> d["a"]
1
>>> d["missing"]
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-3-d4f58b57b715> in <module>()
----> 1 d["missing"]
KeyError: 'missing'
While if we were using a VivoDict, then upon accessing a missing key we
would be provided with an implicitly created empty VivoDict as such:
>>> from vivodict import VivoDict
>>> d = VivoDict.vivify({"a": 1, "b": 2})
>>> d["a"]
1
>>> d["missing"]
{}
Note
Note that instantiation above is not performed simply by passing an existing
dict to VivoDict but instead through the vivify class method
which can recursively convert any arbitrarily-nested dict to a
VivoDict.
Now, while the above doesn't seem to offer anything a simple try-except or
a if "key" not in d wouldn't offer, the VivoDict becomes useful when
dealing with arbitrarily nested dictionaries where there may be several levels
of missing keys. For example:
>>> from vivodict import VivoDict
>>> d = VivoDict({"a": 1, "b": {"c": 2}, "d": {"e": {"f": 3}}})
>>> d["a"]
1
>>> d["b"]["c"]
2
>>> d["d"]["e"]["f"]
3
>>> d["i"]["am"]["missing"]["eh"] = 4
>>> d
{'a': 1,
'b': {'c': 2},
'd': {'e': {'f': 3}},
'i': {'am': {'missing': {'eh': 4}}}}
So, as can be seen, having auto-vivification allows one to nest keys and values to whatever degree.
Warning
The primary caveat of the above functionality is that VivoDict are very
forgiving when it comes to typos which can leads to weird errors. A
mistyped key will simply create a new VivoDict and will allow you to go
down some rabbithole of erroneously typed keys your linter won't get you out
of.
In addition to the above, a few basic convenience methods have been built into
the VivoDict class, mostly cause they make my life easier and lazier.
As I mentioned prior one of my typical use-cases for vivodict is using it
to store nested metrics which I then post to Graphite via simple HTTP requests.
Graphite, however, bases its structure on . delimited names where anything
preceding a . is considered to be a metric folder with the last token being
the metric itself.
Thus, I needed a quick way to flatten a nested dict into a Graphite
compatible version.
The flatten method does exactly that:
>>> d = VivoDict.vivify({"a": 1, "b": {"c": 2}, "d": {"e": {"f": 3}}})
>>> d.flatten()
{'a': 1, 'b.c': 2, 'd.e.f': 3}
Following the same premise as with flatten I needed to quickly 'reset' my
metrics back to 0 between posting cycles.
Hence, replace will replace all 'leaf' node values in what is essentially a
tree with a given value:
>>> d = VivoDict.vivify({"a": 1, "b": {"c": 2}, "d": {"e": {"f": 3}}})
>>> d.replace(replace_with=0)
>>> d
{'a': 0, 'b': {'c': 0}, 'd': {'e': {'f': 0}}}
Warning
As you may have noticed from the above snippet, the replace method
performs an in-place replacement instead of returning a copy of the
original VivoDict with replaced values.
Should you need to maintain an original copy I'd suggest you use the copy
package and its deepcopy function (cause Python passes by reference) as
such:
>>> import copy
>>> original = VivoDict.vivify({"a": 1, "b": {"c": 2}, "d": {"e": {"f": 3}}})
>>> modified = copy.deepcopy(original)
>>> modified.replace(replace_with=0)
>>> original
{'a': 1, 'b': {'c': 2}, 'd': {'e': {'f': 3}}}
>>> modified
{'a': 0, 'b': {'c': 0}, 'd': {'e': {'f': 0}}}
Lastly, I often had to modify all values through a given function, typically
divide them by a number of observation for average metrics which can be easily
done through the apply method which can take any callable as an
argument and replace the original value with its return-value:
>>> d = VivoDict.vivify({"a": 1, "b": {"c": 2}, "d": {"e": {"f": 3}}})
>>> def double(value):
>>> return value * 2
>>> d.apply(double)
>>> d
{'a': 2, 'b': {'c': 4}, 'd': {'e': {'f': 6}}}
>>> d.apply(lambda value: value / 2)
{'a': 1, 'b': {'c': 2}, 'd': {'e': {'f': 3}}}
Warning
Much like replace, the apply method replaces values in-place.