Skip to content

Instantly share code, notes, and snippets.

@mzpqnxow
Last active September 12, 2021 00:31
Show Gist options
  • Save mzpqnxow/b3b8f44fbecc572944266a8974fc5395 to your computer and use it in GitHub Desktop.
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
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
@mzpqnxow
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment