Python对象的可选链接:foo?.bar?.baz

93

在JavaScript中,如果我不确定链中的每个元素是否存在/未定义,我可以使用foo?.bar,如果foo上不存在bar,解释器将会默默地短路它并不会抛出错误。

在Python中有类似的东西吗?目前,我是这样做的:

if foo and foo.bar and foo.bar.baz:
    # do something

我的直觉告诉我这不是检查链中每个元素是否存在的最佳方法。有没有更优雅/Pythonic的方法可以做到这一点?


我的直觉告诉我这不是检查链中每个元素是否存在的最佳方法。有没有更优雅/Pythonic的方法可以做到这一点?

12
你需要的是 PEP 505,可以在 https://www.python.org/dev/peps/pep-0505 找到该文档。 - brandonscript
这正是我正在寻找的,但或许点操作符和下标操作符还未被添加到Python中(尚未?) - Xeoth
也许吧?我不确定,因为我还没有特权使用 Py 3.8,但它似乎已经被批准了? - brandonscript
2
它不在Python 3.8中。PEP 505被标记为Deferred,这意味着没有进展。请参阅https://discuss.python.org/t/pep-505-status/4612上的讨论。 - John Mellor
我相当确定它是对象,而不是字典。话虽如此,它们都不支持可选链接,因此问题对两者来说都是开放的。 - Xeoth
显示剩余2条评论
13个回答

31

如果这是一个字典,您可以使用get(keyname, value)方法

{'foo': {'bar': 'baz'}}.get('foo', {}).get('bar')

5
如果'foo'的值为None,它将会失败。 - Noam Nol
如此繁琐,似乎唯一合理的选择就是使用 pydash。 - undefined

24
您可以使用getattr函数:
getattr(getattr(foo, 'bar', None), 'baz', None)

10
这段代码并不比 OP 的更易读:if foo and foo.bar and foo.bar.baz: - Kuldeep Jain
@KuldeepJain 虽然这种写法可能不太易读,但是 if foo and foo.bar and foo.bar.baz: 是行不通的,因为它会引发一个 AttributeError。这个答案至少像可选链一样工作,如果链中的某个部分缺失,它不会引发异常,而是返回 None - Andria
是的,如果barbaz缺失,它就有意义了。谢谢你澄清这一点。 - Kuldeep Jain

21

最Pythonic的方式是:

try:
    # do something
    ...
except (NameError, AttributeError) as e:
    # do something else
    ...

30
我希望这里有一种方法可以踩Python :'( - gduq

15

你可以使用 Glom。


from glom import glom

target = {'a': {'b': {'c': 'd'}}}
glom(target, 'a.b.c', default=None)  # returns 'd'

https://github.com/mahmoud/glom


这不是可选链,只有在路径存在时才起作用。使用可选链时,您希望它简单地返回None而不是失败。 - svenema
你也可以设置默认值。glom(target, 'a.b.d', default=None) - Sérgio Rafael Siqueira
1
感谢您澄清,这使得它成为一个好的和正确的答案。顺便说一下,在您更新答案之前,我无法点赞。 - svenema
1
请在文本中包含 glom 的 Github 链接。 - gduq

11

需要注意的是,这仅适用于Python 3,因为Python 2变量名称不允许Unicode。 - smac89
太棒了 - vaidik

6

结合我在这里看到的一些东西。

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')

或者你可以扩展getattr,这样你也可以将其用作 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)

使用 rgetattr ,如果路径不存在,它仍然会引发一个 AttributeError,您可以指定自己的默认值,而不是 None


1
optional_chain 的例子有几个 bug。reduce 应该被返回,而 root 不存在,应该是 obj - sutherlandahoy

5
将其他答案结合在一起,编写一个函数可以让代码更易读,并且可以与对象和字典一起使用。
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

使用这个函数,你只需要在第一个参数后面添加键/属性即可。
obj = {'a': {'b': {'c': {'d': 1}}}}
print(optional_chain(obj, 'a', 'b'), optional_chain(obj, 'a', 'z'))

为我们提供:

{'c': {'d': 1}} None

4

类可以重写 __getattr__ 方法来返回缺失属性的默认值:

class Example:
    def __getattr__(self, attr): # only called when missing
        return None

测试它:

>>> ex = Example()
>>> ex.attr = 1
>>> ex.attr
1
>>> ex.missing # evaluates to `None
>>>

然而,这种方式不能进行链式操作:
>>> ex.missing.missing
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'missing'

它也不能处理尝试调用不存在的方法:

>>> ex.impossible()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable

为了解决这个问题,我们可以创建一个代理对象:
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()

返回该值而不是None

class Example:
    def __getattr__(self, attr): # only called when missing
        return GetAnything

现在它可以按照需要进行链接:
>>> Example().missing_attribute.missing_method().whatever
<Missing value>

有人可能会问:“为什么不直接从GetAnything类继承,而是要定义另一个__getattr__?”如果你这样做,那么像Example().missing_method().valid_attribute这样的代码会意外地成功,假设Example实例有一个valid_attribute但没有missing_method - missing_method代理调用将返回相同的Example实例,允许查找valid_attribute。这可能不是你想要的。一旦查找“失败”,保持回到同一个对象是有意义的 - 但第一次不是。 - Karl Knechtel

1
我在工作中遇到了这个问题。Python确实有内置的hasattr函数,可以在尝试访问属性之前进行检查。虽然有些笨拙,但它确实有效。
foo.bar if hasattr(foo, "bar") else None

1

Python 3.10 引入了 match 语句,详见 PEP-634,教程可参考 PEP-636

这个语句允许执行这些“链式”操作,但请注意它们是语句而不是表达式。

例如,OP 可以选择这样做:

match foo:
    case object(bar=object(baz=baz)) if baz:
        # do something with baz

需要使用object的原因是因为所有东西都是它的子类型,因此它总是成功的。然后继续检查属性是否存在,这可能会失败。如果属性不存在,异常不会被抛出,只有case不匹配,它就会移动到下一个(在这种情况下不存在,因此什么也不会发生)。
更现实的例子将检查更具体的内容,例如:
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))

这将输出:

the value
None
None

如果您想将其解构成字典,可以使用以下类似方式:

foo = {'bar': {'baz': 'the value'}}

match foo:
    case {'bar': {'baz': baz}}:
        print(baz)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接