ivaigult
ivaigult

Reputation: 6667

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?

Upvotes: 3

Views: 536

Answers (1)

Martijn Pieters
Martijn Pieters

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:

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

Related Questions