为什么@property比属性访问慢,而字节码相同?

3

考虑下面这段代码:

import timeit
import dis

class Bob(object):
    __slots__ = "_a",

    def __init__(self):
        self._a = "a"

    @property
    def a_prop(self):
        return self._a

bob = Bob()

def return_attribute():
    return bob._a

def return_property():
    return bob.a_prop

print(dis.dis(return_attribute))
print(dis.dis(return_property))

print("attribute:")
print(timeit.timeit("return_attribute()",
                    setup="from __main__ import return_attribute", number=1000000))
print("@property:")
print(timeit.timeit("return_property()",
                    setup="from __main__ import return_property", number=1000000))


易于看出return_attributereturn_property产生相同的字节码:
 17           0 LOAD_GLOBAL              0 (bob)
              3 LOAD_ATTR                1 (_a)
              6 RETURN_VALUE        
None
 20           0 LOAD_GLOBAL              0 (bob)
              3 LOAD_ATTR                1 (a_prop)
              6 RETURN_VALUE        
None

然而,时间是不同的:

attribute:
0.106526851654
@property:
0.210631132126

为什么?
1个回答

10
一个属性被执行为函数调用,而属性查找仅是哈希表(字典)查找。因此,这将始终较慢。
在这里,LOAD_ATTR bytecode 不是固定时间操作。你所缺少的是,LOAD_ATTR 委托属性查找到对象类型; 通过触发 C 代码:
查找栈顶对象的类型,并要求它根据实例生成属性的值。参见ceval.c评估循环部分的LOAD_ATTR,该部分调用PyObject_GetAttr()。对于自定义Python类,这最终会调用_PyObject_GenericGetAttrWithDict()函数(通过type->tp_getattro slotPyObject_GenericGetAttr)。
然后,对象会在其自己的MRO中查找与属性名称匹配的任何描述符对象。如果有这样的对象,并且它是数据描述符,则搜索停止并绑定数据描述符(在其上调用descriptor.__get__()),并返回结果。参见这些行是_PyObject_GenericGetAttrWithDict()
如果没有数据描述符,则查看实例__dict__中是否存在属性名称作为键。如果存在这样的键,则返回相应的值;参见这些行
如果在实例__dict__中没有这样的键,但找到了非数据描述符,则绑定该描述符(在其上调用__get__),并返回结果,在此部分中。
如果在类上定义了__getattr__方法,请调用该方法。参见slot_tp_getattr_hook中的这些行,当您向类添加__getattr__钩子时,它会被安装。
否则,引发AttributeError
一个property对象是一个数据描述符; 它不仅实现了__get__方法,还实现了__set____delete__方法。在实例上调用property__get__会导致property对象调用已注册的getter函数。
有关描述符的更多信息,请参见描述符 HOWTO以及Python datamodel文档的调用描述符部分
字节码没有区别,因为它不是由LOAD_ATTR字节码决定属性是属性还是常规属性。Python是一种动态语言,编译器无法预先知道要访问的属性是否将是属性。您可以随时修改类
class Foo:
    def __init__(self):
        self.bar = 42

f = Foo()
print(f.bar)  # 42

Foo.bar = property(lambda self: 81)
print(f.bar)  # 81

在上面的例子中,当你只有在Foo类的实例f中作为属性存在的bar名称时,通过添加Foo.barproperty对象,我们拦截了名称bar的查找过程,因为property是一个数据描述符,所以可以覆盖任何实例查找。但是,Python无法提前知道这一点,因此无法为属性查找提供不同的字节码。例如,Foo.bar赋值可能发生在完全不相关的模块中。

一个属性被执行时就像一个函数调用 - 我在字节码中没有看到它。 - ivaigult
1
@ivaigult:不会的,而且你不会。 - Martijn Pieters
@ivaigult:字节码是静态的,但属性不是静态的。 - Martijn Pieters

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