Last active
September 12, 2021 00:31
-
-
Save mzpqnxow/b3b8f44fbecc572944266a8974fc5395 to your computer and use it in GitHub Desktop.
Python ChainMap that treats items with None as the value the same as if the associated key was not set
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from collections import ChainMap | |
class NoneChainMap(ChainMap): | |
"""Modified ChainMap to allow items to be treated as unset if they have a specific value (e.g. None) | |
(C) 2021, [email protected], BSD-3-Clause | |
LICENSE: https://opensource.org/licenses/BSD-3-Clause | |
Problem | |
------- | |
A ChainMap works great for scoping dictionaries but it has one annoying limitation. It will happily | |
prioritize a None value over a "real" value. This means that if you have a value initialized to None, | |
it will override other values in the scoped list of dict items | |
Solution | |
-------- | |
Override ChainMap as simply as possible to cause items with None values to be treated the same as if | |
they were unset | |
Example Behavior | |
---------------- | |
In: from collections import ChainMap | |
In: class FChainMap(ChainMap): | |
... this class ... | |
# Create three dictionaries, d0 is the highest priority, d2 is the lowest | |
In: d0 = {'a': None, 'b': None} | |
In: d1 = {'a': 'default', 'b': None} | |
In: d2 = {'a': 'fallback', 'b': 'something'} | |
The undesired behavior provided by ChainMap; the None overrides scope1: | |
In: print(ChainMap(d0, d1, d2)['a']) | |
None | |
The desired behavior provided by NoneChainMap; the None doesn't override scope1: | |
In: print(FChainMap(d0, d1, d2)['a']) | |
default | |
Implementation | |
-------------- | |
It's very simple to override ChainMap to behave how we want | |
There are two simple ways to do this: | |
1. Filtering set operations, never setting the items that have a None value | |
2. Filtering get operations, never returning the items that have a None value | |
This uses #2 as it saves memory and it's very simple and compact. By hooking __init__() | |
and __setitem__(), every possible angle is covered | |
This class is still a fully functional ChainMap. The only thing that is different is how | |
it treats items with a None value. If that is not the case, it's a bug, please tell me :> | |
""" | |
def __init__(self, *maps, unset=None): | |
"""Create the underlying map, filtering out the `unset` value dynamically | |
The value specified by the `unset` kwarg is the value that will be treated as "unset" | |
By default it is None | |
""" | |
self._unset = unset | |
# This prevents the value from being during initialization | |
self.maps = [{k: v for k, v in m.items() if v != unset} for m in maps] | |
def __setitem__(self, key, value): | |
"""Prevent the `unset` value from being set after instantiation""" | |
if value != self._unset: | |
self.maps[0][key] = value |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is useful when you have a dict where uninitialized/unset items have a key but a None value. Rather than having to explicitly filter out the items with a None value, you can use this as a drop-in replacement for
collections.ChainMap