https://glom.readthedocs.io/en/latest/api.html#defaults-with...
e.g.
target = [{"a": {"b": "c"}}, {"a": {"c": "e"}}, {"a": {}}]
results = glom(target, [Coalesce("a.b", "a.c", default="")]) # -> ["c", "e", ""]https://github.com/jmespath/jmespath.py
print(jmespath.search(
expression="some.deep.nested.value",
data={"some": {"deep": {"nested": {"value": 2}}}},
))
prints 2
print(jmespath.search(
expression="some.deep.nested.value2",
data={"some": {"deep": {"nested": {"value": 2}}}},
))
prints NoneI write 'may' because the difference between None and an empty dict may be very subtle and rarely does the API specify with enough precision what are supposed to be the semantics of each case.
It does though, you can chain as many of these as you want:
some_dict.get("level_1_key", {}).get("level_2_key", {}).get("level_3_key", {})...
edit:>> but you still need to check for None with this code
> Not sure what you mean here. You only have to check for None if you use `.get("key")` and don't provide a fallback value.
GP was talking about `{"foo": None}` and trying to drill deeper, which I misunderstood. Still, a simple try/except allows you to short-circuit the deeply nested access.
>>> foo = {"foo": None}
>>> print(foo.get('foo', {}).get('bar'))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'get'
>>> print((foo.get('foo') or {}).get('bar'))
None> Blowing up on `None` is an easy short-circuit.
The thing being discussed is attempting to access deeply nested values, so short-circuiting here is a win-win. I.e., you wouldn't want to unnecessarily traverse tons of empty dictionaries using the `.get() or {}` trick.
This has the side effect of replacing falsy values like False or 0 with an empty dictionary instead of giving you the actual value. For the exact intended behaviour, you do need to explicitly check for None instead of using a short circuit trick with or.
I would say the only reasonable choice is Optional[dict], in which case it should be sufficient, but Python being Python, it could be anything. And in these cases you need to handle all cases way more carefully.
def safe_navigation(obj, path: List[str], default=None):
try:
for key in path:
obj = obj[key]
return obj
except KeyError:
return default
Yeah. I thought it would just be a try-except, but that turned out pretty ugly.But really, how do you know if your path is missing or the object at the end of the path is None, without using exceptions for communicating this?
def safe_navigation(obj: Any, *path: str, default: Any = None, strict: bool = False) -> Any:
sentinel = object() if strict else None
for key in path:
obj = obj.get(key, sentinel)
if obj is sentinel:
return default
return obj
The only new disadvantage of this is that it only works on mappings and no longer works on sequences.But I really don't like having this much `Any`; it's generally a sign of poor design.
This isn't like C++ with its intrusive `=`.
$ python3
Python 3.11.6 (main, Oct 2 2023, 20:46:14) [Clang 14.0.3 (clang-1403.0.22.14.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from typing import List
>>> def safe_navigation(obj, path: List[str], default=None):
... try:
... for key in path:
... obj = obj[key]
... return obj
... except KeyError:
... return default
...
>>> obj = {1: {1: {1: 2}}}
>>> safe_navigation(obj, [1,1,1])
2
>>> obj
{1: {1: {1: 2}}}I saw some suggestions for overridable behaviour in the data model for None-like objects, but didn't scrutinise them closely. Either way, it seems controversial. On the one hand, the ability to override operators is really key to Python's design, but on the other it could lead to rather surprising behaviour from overeager overriding. (But to be fair, this is true of other, more common operators too.)
So, for now we're stuck with `if _ is not None else`, I suppose!
What's actually a bit odd about Python missing that operator is that its `or` operator acts as a rough equivalent of C#'s null-coalescing (`??`) operator. To me the safe navigation operator goes hand-in-hand with that.
Python `or` is C# `||` plus type coercion. It is not even roughly `??` because important non-None Python values are falsey (0, False, empty collections).
> python3 -c 'print(None or "hello")'
hello
It evaluates to the left if that's truthy otherwise it evaluates to the right, similar to how `??` evaluates to the left if that's non-null otherwise the right.In C# even if `||` automatically coerced the types, the output will always be a bool.