关于python:检查属性是否存在的最佳方法是什么?

Which is the best way to check for the existence of an attribute?

本问题已经有最佳答案,请猛点这里访问。

哪种方法更好地检查属性的存在?

Jarret Hardie提供了以下答案:

1
2
if hasattr(a, 'property'):
    a.property

我看到它也可以这样做:

1
2
if 'property' in a.__dict__:
    a.property

一种方法通常比其他方法更常用吗?


没有"最佳"方法,因为您绝不只是检查属性是否存在;它始终是某个较大程序的一部分。有几种正确的方法和一种明显的错误方法。

错误的方式

1
2
if 'property' in a.__dict__:
    a.property

下面是一个演示,说明此技术失败:

1
2
3
4
5
6
7
8
9
class A(object):
    @property
    def prop(self):
        return 3

a = A()
print"'prop' in a.__dict__ =", 'prop' in a.__dict__
print"hasattr(a, 'prop') =", hasattr(a, 'prop')
print"a.prop =", a.prop

输出:

1
2
3
'prop' in a.__dict__ = False
hasattr(a, 'prop') = True
a.prop = 3

大多数时候,你不想和__dict__搞混。它是做特殊事情的一个特殊属性,检查属性是否存在是很平常的。

EAFP方式

在Python中,一个常见的习语是"请求宽恕比请求允许更容易",简称EAFP。您将看到许多使用这种习惯用法的Python代码,而不仅仅是为了检查属性的存在性。

1
2
3
4
5
6
7
8
9
10
# Cached attribute
try:
    big_object = self.big_object
    # or getattr(self, 'big_object')
except AttributeError:
    # Creating the Big Object takes five days
    # and three hundred pounds of over-ripe melons.
    big_object = CreateBigObject()
    self.big_object = big_object
big_object.do_something()

注意,这与打开可能不存在的文件完全相同。

1
2
3
4
5
6
7
8
try:
    f = open('some_file', 'r')
except IOError as ex:
    if ex.errno != errno.ENOENT:
        raise
    # it doesn't exist
else:
    # it does and it's open

另外,用于将字符串转换为整数。

1
2
3
4
5
try:
    i = int(s)
except ValueError:
    print"Not an integer! Please try again."
    sys.exit(1)

甚至导入可选模块…

1
2
3
4
try:
    import readline
except ImportError:
    pass

路比路

当然,hasattr方法也有效。这种技巧被称为"跳跃前看",简称lbyl。

1
2
3
4
5
# Cached attribute
if not hasattr(self, 'big_object'):
    big_object = CreateBigObject()
    self.big_object = CreateBigObject()
big_object.do_something()

(对于3.2之前的Python版本,hasattr内置实际上在异常方面表现得很奇怪--它会捕获不应该捕获的异常--但这可能与此无关,因为此类异常不太可能。hasattr技术也比try/except慢,但你不经常称之为关心,差别也不大。最后,hasattr不是原子的,因此如果另一个线程删除了属性,它可能会抛出AttributeError,但这是一个牵强的场景,无论如何,您需要非常小心地处理线程。我认为这三个差异中的任何一个都不值得担心。)

使用hasattrtry/except简单得多,只要您只需要知道属性是否存在。对我来说,最大的问题是lbyl技术看起来"奇怪",因为作为一个Python程序员,我更习惯于阅读EAFP技术。如果您重写上述示例以使它们使用LBYL样式,您将得到笨拙、完全不正确或太难编写的代码。

1
2
3
4
5
6
# Seems rather fragile...
if re.match('^(:?0|-?[1-9][0-9]*)$', s):
    i = int(s)
else:
    print"Not an integer! Please try again."
    sys.exit(1)

而lbyl有时完全不正确:

1
2
3
4
if os.path.isfile('some_file'):
    # At this point, some other program could
    # delete some_file...
    f = open('some_file', 'r')

如果你想写一个导入可选模块的lbyl函数,请便…听起来这个功能就像一个怪物。

GATTAL方式

如果您只需要一个默认值,那么getattrtry/except的较短版本。

1
x = getattr(self, 'x', default_value)

如果默认值的构建成本很高,那么您将得到如下结果:

1
2
3
4
x = getattr(self, 'attr', None)
if x is None:
    x = CreateDefaultValue()
    self.attr = x

或者如果None是一个可能的值,

1
2
3
4
5
6
sentinel = object()

x = getattr(self, 'attr', sentinel)
if x is sentinel:
    x = CreateDefaultValue()
    self.attr = x

结论

在内部,getattrhasattr内置设备仅使用try/except技术(用C编写的除外)。所以他们的行为方式都是一样的,正确的选择是由于环境和风格的问题。

try/exceptEAFP代码总是以错误的方式摩擦一些程序员,而hasattr/getattrLBYL代码会使其他程序员感到厌烦。它们都是正确的,而且通常没有真正令人信服的理由来选择其中一个。(然而,其他程序员对属性未定义的行为感到厌恶,有些程序员甚至害怕在python中可能有未定义的属性。)


方法是用hasattr()

a.__dict__很难看,在很多情况下都不起作用。hasattr()实际上试图获取属性并在内部捕获AttributeError,因此即使定义自定义__getattr__()方法,它也能工作。

为了避免两次请求属性,可以使用getattr()的第三个参数:

1
2
3
4
5
6
7
8
not_exist = object()

# ...
attr = getattr(obj, 'attr', not_exist)
if attr is not_exist:
   do_something_else()
else:
   do_something(attr)

如果您的情况更合适的话,您可以使用默认值而不是not_existsentinel。

我不喜欢try: do_something(x.attr)
except AttributeError: ..
,它可能会把AttributeError隐藏在do_something()函数中。

*在python 3.1之前,如果不希望使用getattr(),那么hasattr()抑制了所有的异常(不仅仅是AttributeError)。


hasattr()是做这件事的方法。学习它,热爱它。

另一种可能的方法是检查变量名是否在locals()globals()中:

1
2
3
4
if varName in locals() or in globals():
    do_something()
else:
    do_something_else()

我个人不喜欢为了检查某些东西而捕捉异常。它看起来和感觉都很难看。这与检查字符串是否只包含数字相同:

1
2
3
4
5
6
s ="84984x"
try:
    int(s)
    do_something(s)
except ValueError:
    do_something_else(s)

而不是轻轻地使用s.isdigit()。EWW。


这个问题很老,但确实需要一个很好的答案。对于一个短程序,我会说使用自定义函数!

下面是一个例子。它并不适用于所有应用程序,但适用于我的应用程序,用于解析来自无数API的响应并使用django。很容易根据每个人自己的需求进行调整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from django.core.exceptions import ObjectDoesNotExist
from functools import reduce

class MultipleObjectsReturned(Exception):
    pass

def get_attr(obj, attr, default, asString=False, silent=True):
   """
    Gets any attribute of obj.
    Recursively get attributes by separating attribute names with the .-character.        
    Calls the last attribute if it's a function.

    Usage: get_attr(obj, 'x.y.z', None)
   """

    try:
        attr = reduce(getattr, attr.split("."), obj)
        if hasattr(attr, '__call__'):
            attr = attr()
        if attr is None:
            return default
        if isinstance(attr, list):
            if len(attr) > 1:
                logger.debug("Found multiple attributes:" + str(attr))
                raise MultipleObjectsReturned("Expected a single attribute")
            else:
                return str(attr[0]) if asString else attr[0]
        else:
            return str(attr) if asString else attr
    except AttributeError:
        if not silent:
            raise
        return default
    except ObjectDoesNotExist:
        if not silent:
            raise
        return default