考虑以下这段代码:
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_attribute
和return_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
为什么?
答案 0 :(得分:8)
将属性作为函数调用执行,而属性查找只是哈希表(字典)查找。是的,那总是会慢一些。
此处的LOAD_ATTR
字节码不是固定时间的操作。您缺少的是LOAD_ATTR
将属性查找委托给对象类型;通过触发以下C代码:
ceval.c
evaluation loop section for LOAD_ATTR
,它调用PyObject_GetAttr()
。对于自定义Python类,这最终会调用_PyObject_GenericGetAttrWithDict()
function(通过type->tp_getattro
slot和PyObject_GenericGetAttr
。descriptor.__get__()
)并返回结果。参见these lines is _PyObject_GenericGetAttrWithDict()
。__dict__
中将属性名称作为键。如果存在这样的密钥,则返回相应的值;否则,返回0。参见these lines。__dict__
中没有这样的键,但是找到了一个非数据描述符,则绑定该描述符(在其上调用__get__
),并返回结果,在this section中。__getattr__
方法,请调用该方法。请参见these lines in slot_tp_getattr_hook
,当您向类中添加__getattr__
钩子时将安装该钩子。AttributeError
。 property
对象是数据描述符;它不仅实现__get__
,而且实现__set__
和__delete__
方法。使用实例在__get__
上调用property
会导致property
对象调用已注册的getter函数。
有关描述符以及Python数据模型描述的Descriptor HOWTO的更多信息,请参见Invoking Descriptors section。
字节码没什么不同,因为不能由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
在上面的示例中,当您以bar
名称开头时,通过添加f
{{仅在类Foo
的{{1}}实例上作为属性存在。 1}}对象我们截取了名称Foo.bar
的查找过程,因为property
是数据描述符,因此可以覆盖所有实例查找。 但是Python无法事先知道这一点,因此无法为属性查找提供不同的字节码。例如,bar
分配可能发生在一个完全不相关的模块中。