Reputation: 1599
In JavaScript, if I'm not sure whether every element of the chain exists/is not undefined, I can do foo?.bar
, and if bar
does not exist on foo
, the interpreter will silently short circuit it and not throw an error.
Is there anything similar in Python? For now, I've been doing it like this:
if foo and foo.bar and foo.bar.baz:
# do something
My intuition tells me that this isn't the best way to check whether every element of the chain exists. Is there a more elegant/Pythonic way to do this?
Upvotes: 114
Views: 48540
Reputation: 19
I ran into this at work. Python does have the hasattr
built in, which can perform the check before you try to access an attribute. It's clunky, but it works:
foo.bar if hasattr(foo, "bar") else None
Upvotes: 1
Reputation: 258
Check out safebag.
I was just about to publish something like this, but found that there's already this package
from dataclasses import dataclass
import typing
from safebag import chain, get_value
@dataclass
class Node:
data: int
node: typing.Optional["Node"]
nodes = Node(
data=1,
node=Node(
data=2,
node=None
)
)
# construct chain
chain(nodes)
# ChainProxy(data_object=Node(data=1, node=Node(data=2, node=None)), bool_hook=False)
# access existing data
get_value(chain(nodes).node.data)
# 2
# chain for non existing data
get_value(chain(nodes).foo.bar.baz)
# None
Upvotes: 3
Reputation: 11
I use reduce to achieve Javascript-like optional chaining in Python
from functools import reduce
data_dictionary = {
'foo': {
'bar': {
'buzz': 'lightyear'
},
'baz': {
'asd': 2023,
'zxc': [
{'patrick': 'star'},
{'spongebob': 'squarepants'}
],
'qwe': ['john', 'sarah']
}
},
'hello': {
'world': 'hello world',
},
}
def optional_chaining_v1(dictionary={}, *property_list):
def reduce_callback(current_result, current_dictionary):
if current_result is None:
return dictionary.get(current_dictionary)
if type(current_result) != dict:
return None
return current_result.get(current_dictionary)
return reduce(reduce_callback, property_list, None)
# or in one line
optional_chaining_v1 = lambda dictionary={}, *property_list: reduce(lambda current_result, current_dictionary: dictionary.get(current_dictionary) if current_result is None else None if type(current_result) != dict else current_result.get(current_dictionary), property_list, None)
# usage
optional_chaining_v1_result1 = optional_chaining_v1(data_dictionary, 'foo', 'bar', 'baz')
print('optional_chaining_v1_result1:', optional_chaining_v1_result1)
optional_chaining_v1_result2 = optional_chaining_v1(data_dictionary, 'foo', 'bar', 'buzz')
print('optional_chaining_v1_result2:', optional_chaining_v1_result2)
# optional_chaining_v1_result1: None
# optional_chaining_v1_result2: lightyear
def optional_chaining_v2(dictionary={}, list_of_property_string_separated_by_dot=''):
property_list = list_of_property_string_separated_by_dot.split('.')
def reduce_callback(current_result, current_dictionary):
if current_result is None:
return dictionary.get(current_dictionary)
if type(current_result) != dict:
return None
return current_result.get(current_dictionary)
return reduce(reduce_callback, property_list, None)
# or in one line
optional_chaining_v2 = lambda dictionary={}, list_of_property_string_separated_by_dot='': reduce(lambda current_result, current_dictionary: dictionary.get(current_dictionary) if current_result is None else None if type(current_result) != dict else current_result.get(current_dictionary), list_of_property_string_separated_by_dot.split('.'), None)
# usage
optional_chaining_v2_result1 = optional_chaining_v2(data_dictionary, 'foo.bar.baz')
print('optional_chaining_v2_result1:', optional_chaining_v2_result1)
optional_chaining_v2_result2 = optional_chaining_v2(data_dictionary, 'foo.bar.buzz')
print('optional_chaining_v2_result2:', optional_chaining_v2_result2)
# optional_chaining_v2_result1: None
# optional_chaining_v2_result2: lightyear
Upvotes: 0
Reputation: 16184
Python 3.10 introduced the match
statement in PEP-634, with the tutorial in PEP-636 being a nice reference.
This statement allow these sorts of "chained" operations to be performed, but note that they are statements and not expressions.
For example, OP could instead do:
match foo:
case object(bar=object(baz=baz)) if baz:
# do something with baz
The reason for needing object
is that everything is a subtype of it and hence it always succeeds. It then goes on to check that the attribute exists, which might fail. Exceptions wouldn't be thrown if the attribute didn't exist, just the case
wouldn't match and it would move onto the next one (which in this case doesn't exist, so nothing would be done).
A more realistic example would check something more specific, e.g.:
from collections import namedtuple
Foo = namedtuple('Foo', ['bar'])
Bar = namedtuple('Bar', ['baz'])
def fn(x):
match x:
case Foo(bar=Bar(baz=baz)):
return baz
print(fn(Foo(bar=Bar(baz='the value'))))
print(fn(None))
print(fn(1))
which would output:
the value
None
None
If instead you wanted to destructure into dictionaries, you might use something like:
foo = {'bar': {'baz': 'the value'}}
match foo:
case {'bar': {'baz': baz}}:
print(baz)
Upvotes: 2
Reputation: 61537
Here's some syntactic sugar to make chaining with getattr
look more like the fluent interfaces of other languages. It's definitely not "Pythonic", but it allows for something simpler to write.
The idea is to abuse the @
operator added in Python 3.5 (to support matrix multiplication in Numpy). We define a class r
such that its instances, when matrix-multiplied on the right of another object, invoke getattr
. (The combination @r
, of course, is read "attr".)
class r:
def __init__(self, name, value=None):
self._name = name
self._value = value
def __rmatmul__(self, obj):
return getattr(obj, self._name, self._value)
Now we can chain attribute accesses easily, without having to modify any other classes (and of course it works on built-in types):
>>> 'foo'@r('bar')@r('baz') # None
>>>
However, the order of operations is inconvenient with method calls:
>>> 'foo bar'@r('split')()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'r' object is not callable
>>> ('foo bar'@r('split'))()
['foo', 'bar']
Upvotes: 3
Reputation: 61537
Classes can override __getattr__
to return a default value for missing attributes:
class Example:
def __getattr__(self, attr): # only called when missing
return None
Testing it:
>>> ex = Example()
>>> ex.attr = 1
>>> ex.attr
1
>>> ex.missing # evaluates to `None
>>>
However, this will not allow for chaining:
>>> ex.missing.missing
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'missing'
Nor will it deal with attempts to call methods that are absent:
>>> ex.impossible()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable
To fix this, we can make a proxy object:
class GetAnything:
def __getattr__(self, attr):
return self
def __call__(self, *args, **kwargs): # also allow calls to work
return self
def __repr__(self):
return '<Missing value>'
# Reassign the name to avoid making more instances
GetAnything = GetAnything()
And return that instead of None
:
class Example:
def __getattr__(self, attr): # only called when missing
return GetAnything
Now it chains as desired:
>>> Example().missing_attribute.missing_method().whatever
<Missing value>
Upvotes: 4
Reputation: 2845
Combining a few things I see here.
from functools import reduce
def optional_chain(obj, keys):
try:
return reduce(getattr, keys.split('.'), obj)
except AttributeError:
return None
optional_chain(foo, 'bar.baz')
Or instead extend getattr
so you can also use it as a drop-in replacement for getattr
from functools import reduce
def rgetattr(obj, attr, *args):
def _getattr(obj, attr):
return getattr(obj, attr, *args)
return reduce(_getattr, attr.split('.'), obj)
With rgetattr
it can still raise an AttributeError
if the path does not exist, and you can specify your own default instead of None.
Upvotes: 6
Reputation: 171
You can use the Glom.
from glom import glom
target = {'a': {'b': {'c': 'd'}}}
glom(target, 'a.b.c', default=None) # returns 'd'
https://github.com/mahmoud/glom
Upvotes: 17
Reputation: 430
Combining some of the other answers into a function gives us something that's easily readable and something that can be used with objects and dictionaries.
def optional_chain(root, *keys):
result = root
for k in keys:
if isinstance(result, dict):
result = result.get(k, None)
else:
result = getattr(result, k, None)
if result is None:
break
return result
Using this function you'd just add the keys/attributes after the first argument.
obj = {'a': {'b': {'c': {'d': 1}}}}
print(optional_chain(obj, 'a', 'b'), optional_chain(obj, 'a', 'z'))
Gives us:
{'c': {'d': 1}} None
Upvotes: 5
Reputation: 12374
If it's a dictionary you can use get(keyname, value)
{'foo': {'bar': 'baz'}}.get('foo', {}).get('bar')
Upvotes: 32
Reputation: 1375
Most pythonic way is:
try:
# do something
...
except (NameError, AttributeError) as e:
# do something else
...
Upvotes: 24
Reputation: 1919
You can use getattr
:
getattr(getattr(foo, 'bar', None), 'baz', None)
Upvotes: 28