pythoninstance-variables

Why is accessing instance attribute slower than local?


import timeit

class Hello():
    def __init__(self):
        self.x = 5
    def get_local_attr(self):
        x = self.x
        # 10x10
        x;x;x;x;x;x;x;x;x;x;
        x;x;x;x;x;x;x;x;x;x;
        x;x;x;x;x;x;x;x;x;x;
        x;x;x;x;x;x;x;x;x;x;
        x;x;x;x;x;x;x;x;x;x;
        x;x;x;x;x;x;x;x;x;x;
        x;x;x;x;x;x;x;x;x;x;
        x;x;x;x;x;x;x;x;x;x;
        x;x;x;x;x;x;x;x;x;x;
        x;x;x;x;x;x;x;x;x;x;
    def get_inst_attr(self):
        # 10x10
        self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;
        self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;
        self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;
        self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;
        self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;
        self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;
        self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;
        self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;
        self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;
        self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;

if __name__ == '__main__':
    obj = Hello()
    print('Accessing Local Attribute:', min(timeit.Timer(obj.get_local_attr)
    .repeat(repeat=5)))
    print('Accessing Instance Attribute:', min(timeit.Timer(obj.get_inst_attr)
    .repeat(repeat=5)))

Results from my computer:

Accessing Local Attribute: 0.686281020000024

Accessing Instance Attribute: 3.7962001440000677

Why does this happen? Moreover, is it a good practice to localise the instance variable before using it?


Solution

  • Every time python looks up a variable, you pay a little (LOAD_FAST op code). Every time you look up an attribute on an existing object, you pay a little more (LOAD_ATTR op code). e.g.

    >>> def f1(self):
    ...   x = self.x
    ...   x
    ... 
    >>> def f2(self):
    ...   self.x
    ...   self.x
    ... 
    >>> dis.dis(f1)
      2           0 LOAD_FAST                0 (self)
                  3 LOAD_ATTR                0 (x)
                  6 STORE_FAST               1 (x)
    
      3           9 LOAD_FAST                1 (x)
                 12 POP_TOP             
                 13 LOAD_CONST               0 (None)
                 16 RETURN_VALUE        
    >>> dis.dis(f2)
      2           0 LOAD_FAST                0 (self)
                  3 LOAD_ATTR                0 (x)
                  6 POP_TOP             
    
      3           7 LOAD_FAST                0 (self)
                 10 LOAD_ATTR                0 (x)
                 13 POP_TOP             
                 14 LOAD_CONST               0 (None)
                 17 RETURN_VALUE        
    >>> 
    

    Even if you don't know how to read python disassembled bytecode, you can see that there is more stuff being done for f2 than for f1.

    Also, note that not all op codes are the same. LOAD_FAST is basically a array lookup in the local scope (so it is FAST as the name implies). LOAD_ATTR is (on the other hand) a bit slower as it translates to a function call (__getattribute__) that (usually) does a dictionary lookup.


    As far as what is "best practice", do what reads the easiest. I think that it's pretty customary to use self unless you demonstrate that there is a noticeable performance gain by avoiding it, but I don't think that is a hard rule.