在Python中检查成员存在性

27 投票
5 回答
19015 浏览
提问于 2025-04-11 09:34

我经常想检查一个对象里是否有某个成员。比如说,在一个函数里创建一个单例(也就是只创建一个实例)。为此,你可以用 hasattr 这样做:

class Foo(object):
    @classmethod
    def singleton(self):
        if not hasattr(self, 'instance'):
            self.instance = Foo()
        return self.instance

不过你也可以这样做:

class Foo(object):
    @classmethod
    def singleton(self):
        try:
            return self.instance
        except AttributeError:
            self.instance = Foo()
            return self.instance

这两种方法哪个更好呢?

补充:我加了 @classmethod ... 但请注意,这个问题 不是 关于如何创建单例,而是如何检查一个对象里是否有某个成员。

补充:在这个例子中,典型的用法是:

s = Foo.singleton()

这样 s 就是一个类型为 Foo 的对象,每次都是同一个。而且,通常这个方法会被调用很多次。

5 个回答

5

这要看什么情况是“典型”的,因为异常情况应该用来处理不寻常的情况。所以,如果通常情况下instance属性应该存在,那就用第二种代码风格。如果没有instance和有instance一样常见,那就用第一种风格。

在创建单例的具体情况下,我倾向于使用第一种风格,因为第一次创建单例是一个典型的用例。:-)

11

我刚刚试着测量了一下时间:

class Foo(object):
    @classmethod
    def singleton(self):
        if not hasattr(self, 'instance'):
            self.instance = Foo()
        return self.instance



class Bar(object):
    @classmethod
    def singleton(self):
        try:
            return self.instance
        except AttributeError:
            self.instance = Bar()
            return self.instance



from time import time

n = 1000000
foo = [Foo() for i in xrange(0,n)]
bar = [Bar() for i in xrange(0,n)]

print "Objs created."
print


for times in xrange(1,4):
    t = time()
    for d in foo: d.singleton()
    print "#%d Foo pass in %f" % (times, time()-t)

    t = time()
    for d in bar: d.singleton()
    print "#%d Bar pass in %f" % (times, time()-t)

    print

在我的电脑上:

Objs created.

#1 Foo pass in 1.719000
#1 Bar pass in 1.140000

#2 Foo pass in 1.750000
#2 Bar pass in 1.187000

#3 Foo pass in 1.797000
#3 Bar pass in 1.203000

看起来使用try/except会更快。对我来说,这种写法也更容易理解。不过这要看具体情况,这个测试很简单,可能你需要一个更复杂的测试。

22

这两种方法是不同的:第一种是 LBYL(先看再跳),第二种是 EAFP(问原谅比问许可更简单)。

在 Python 的圈子里,大家通常认为 EAFP 更好,理由是“如果在你检查文件是否存在和你自己尝试创建文件之间,有其他程序创建了这个文件怎么办?”虽然这个理由在这里不太适用,但大致意思就是:异常情况不应该被看得太过特别。

从性能的角度来看——因为在 CPython 中设置异常管理器(使用 try 关键字)是非常便宜的,而创建异常(使用 raise 关键字和内部异常创建)相对来说是比较昂贵的——使用第二种方法时,异常只会被触发一次;之后你就可以直接使用这个属性了。

撰写回答