Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is @property slower that an attribute while bytecode is the same

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?

like image 653
ivaigult Avatar asked Nov 28 '25 18:11

ivaigult


1 Answers

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:

  • Finds the type of the object on the top of the stack, and asks it to produce the value for the attribute, given the instance. See the 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.
  • The object then looks through its own MRO for any descriptor objects that match the attribute name. If there is such an object, and it is a data descriptor, search stops and the data descriptor is bound (descriptor.__get__() is called on it) and the result returned. See these lines is _PyObject_GenericGetAttrWithDict().
  • If there are no data descriptors, looks at the instance __dict__ for the attribute name as a key. If such a key exists, the corresponding value is returned; see these lines.
  • If there is no such key in the instance __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.
  • If there is a __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.
  • otherwise, raise 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.

like image 51
Martijn Pieters Avatar answered Dec 01 '25 07:12

Martijn Pieters