为什么 @属性 在字节码相同的情况下比属性慢
Why is @property slower that an attribute while bytecode is the same
考虑这段代码:
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
为什么?
A 属性 作为 函数调用 执行,而属性查找只是散列 table(字典)查找。所以是的,那总是会更慢。
这里的LOAD_ATTR
字节码不是fixed-time操作。您缺少的是 LOAD_ATTR
将属性查找委托给对象类型 ;通过触发 C 代码:
- 查找堆栈顶部对象的类型,并要求它为给定实例的属性生成值。见
ceval.c
evaluation loop section for LOAD_ATTR
, which invokes PyObject_GetAttr()
. For custom Python classes this ends up calling the _PyObject_GenericGetAttrWithDict()
function (via the type->tp_getattro
slot and PyObject_GenericGetAttr
.
- 对象然后通过自己的 MRO 查找与属性名称匹配的任何 描述符对象。如果存在这样一个对象,并且它是一个数据描述符,则搜索停止并绑定数据描述符(
descriptor.__get__()
is called on it) and the result returned. See these lines is _PyObject_GenericGetAttrWithDict()
.
- 如果没有数据描述符,则查看实例
__dict__
以获取属性名称作为键。如果存在这样的键,则返回相应的值;参见 these lines。
- 如果实例
__dict__
中没有这样的键,但找到了 non-data 描述符,则绑定该描述符(在其上调用 __get__
),并且结果返回,在this section.
- 如果在 class 上定义了
__getattr__
方法,请调用该方法。请参阅 these lines in slot_tp_getattr_hook
,它是在将 __getattr__
挂钩添加到 class 时安装的。
- 否则,提高
AttributeError
。
一个property
对象是一个数据描述符;它不仅实现了 __get__
,还实现了 __set__
和 __delete__
方法。使用实例在 property
上调用 __get__
会导致 property
对象调用已注册的 getter 函数。
请参阅 Python 数据模型文档的 Descriptor HOWTO for more information on descriptors, as well as the Invoking Descriptors section。
字节码 没有区别 因为它不是由 LOAD_ATTR
字节码来决定属性是 属性 还是常规属性. Python 是一种动态语言,编译器无法预先知道 所访问的属性是否为 属性。您可以随时更改 class :
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
名称开始时,它仅作为 class Foo
的 f
实例中的一个属性存在,通过添加 Foo.bar
property
对象我们拦截了名称 bar
的查找过程,因为 property
是一个数据描述符,因此可以覆盖任何实例查找。 但是Python无法提前知道这一点,因此无法为属性查找提供不同的字节码。例如,Foo.bar
赋值可能发生在完全不相关的模块中。
考虑这段代码:
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
为什么?
A 属性 作为 函数调用 执行,而属性查找只是散列 table(字典)查找。所以是的,那总是会更慢。
这里的LOAD_ATTR
字节码不是fixed-time操作。您缺少的是 LOAD_ATTR
将属性查找委托给对象类型 ;通过触发 C 代码:
- 查找堆栈顶部对象的类型,并要求它为给定实例的属性生成值。见
ceval.c
evaluation loop section forLOAD_ATTR
, which invokesPyObject_GetAttr()
. For custom Python classes this ends up calling the_PyObject_GenericGetAttrWithDict()
function (via thetype->tp_getattro
slot andPyObject_GenericGetAttr
. - 对象然后通过自己的 MRO 查找与属性名称匹配的任何 描述符对象。如果存在这样一个对象,并且它是一个数据描述符,则搜索停止并绑定数据描述符(
descriptor.__get__()
is called on it) and the result returned. See these lines is_PyObject_GenericGetAttrWithDict()
. - 如果没有数据描述符,则查看实例
__dict__
以获取属性名称作为键。如果存在这样的键,则返回相应的值;参见 these lines。 - 如果实例
__dict__
中没有这样的键,但找到了 non-data 描述符,则绑定该描述符(在其上调用__get__
),并且结果返回,在this section. - 如果在 class 上定义了
__getattr__
方法,请调用该方法。请参阅 these lines inslot_tp_getattr_hook
,它是在将__getattr__
挂钩添加到 class 时安装的。 - 否则,提高
AttributeError
。
一个property
对象是一个数据描述符;它不仅实现了 __get__
,还实现了 __set__
和 __delete__
方法。使用实例在 property
上调用 __get__
会导致 property
对象调用已注册的 getter 函数。
请参阅 Python 数据模型文档的 Descriptor HOWTO for more information on descriptors, as well as the Invoking Descriptors section。
字节码 没有区别 因为它不是由 LOAD_ATTR
字节码来决定属性是 属性 还是常规属性. Python 是一种动态语言,编译器无法预先知道 所访问的属性是否为 属性。您可以随时更改 class :
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
名称开始时,它仅作为 class Foo
的 f
实例中的一个属性存在,通过添加 Foo.bar
property
对象我们拦截了名称 bar
的查找过程,因为 property
是一个数据描述符,因此可以覆盖任何实例查找。 但是Python无法提前知道这一点,因此无法为属性查找提供不同的字节码。例如,Foo.bar
赋值可能发生在完全不相关的模块中。