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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With