Reputation: 6667
Consider this chunk of code:
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))
It is easy to see that return_attribute
and return_property
result in the same byte code:
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
However, timings are different:
attribute:
0.106526851654
@property:
0.210631132126
Why?
Upvotes: 3
Views: 536
Reputation: 1122252
A property is executed as a function call, while the attribute lookup is merely a hash table (dictionary) lookup. So yes, that'll always be slower.
The LOAD_ATTR
bytecode is not a fixed-time operation here. What you are missing is that LOAD_ATTR
delegates attribute lookups to the object type; by triggering C code that:
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
.descriptor.__get__()
is called on it) and the result returned. See these lines is _PyObject_GenericGetAttrWithDict()
.__dict__
for the attribute name as a key. If such a key exists, the corresponding value is returned; see these lines.__dict__
, but there was a non-data descriptor found, then that descriptor is bound (__get__
is called on it), and the result is returned, in this section.__getattr__
method defined on the class, call that method. See these lines in slot_tp_getattr_hook
, which is installed when you add a __getattr__
hook to a class.AttributeError
.A property
object is a data descriptor; it implements not only __get__
but also the __set__
and __delete__
methods. Calling __get__
on the property
with an instance causes the property
object to call the registered getter function.
See the Descriptor HOWTO for more information on descriptors, as well as the Invoking Descriptors section of the Python datamodel documentation.
The bytecode doesn't differ because it is not up to the LOAD_ATTR
bytecode to decide if the attribute is a property or a regular attribute. Python is a dynamic language, and the compiler can't know up front if the attribute accessed is going to be a property. You can alter your class at any time:
class Foo:
def __init__(self):
self.bar = 42
f = Foo()
print(f.bar) # 42
Foo.bar = property(lambda self: 81)
print(f.bar) # 81
In the above example, while you start with the bar
name only existing as an attribute on the f
instance of class Foo
, by adding the Foo.bar
property
object we intercepted the lookup procedure for the name bar
, because a property
is a data descriptor and so gets to override any instance lookups. But Python can't know this in advance and so can't provide a different bytecode for property lookups. The Foo.bar
assignment could happen in a completely unrelated module, for example.
Upvotes: 10