Python | Why is accessing instance attribute slower than local?
Asked Answered
O

3

11
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?

Ovolo answered 19/2, 2015 at 1:7 Comment(6)
Just a sidenote, you can specify how often something is executed with timeit (you don't need to write it down 100 times)Chewning
If I call the class method too many times, there will be function call overhead, right?Ovolo
in both cases .... it is an insignificant enough amount of time that I can guarantee this will not be your programs bottle neck .... not sure what you mean by localize ... but usually it has to do with translations ... not makeing a class variable a local variableZuleika
What do you mean with "translations"?Ovolo
google "localization programming" and you will see ...Zuleika
I see. What I mean localise is to make local of something, in my case is the instance variable. Sorry with that, I thought the meaning was obvious.Ovolo
M
13

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.

Missive answered 19/2, 2015 at 1:17 Comment(0)
L
10

Because local variables are simply accessed using a single byte code step LOAD_FAST, on the other hand self.x will require first looking up for self using LOAD_FAST and then access x on it, that too is complicated as Python has to first check whether it is data descriptor or just simple instance attribute and based on that then its value is fetched.

Usually it is good idea to cache such heavily repeated calls when dealing with methods in CPython, because otherwise every time a new bound object is created. I've hardly seen a case where normal attribute was cached to get some performance benefits. Other implementations like PyPy and Pyston have their own way of speeding up attribute lookups. From data model page:

Note that the transformation from function object to (unbound or bound) method object happens each time the attribute is retrieved from the class or instance. In some cases, a fruitful optimization is to assign the attribute to a local variable and call that local variable.

One example for this would be list.append(Also see: https://hg.python.org/cpython/file/f7fd2776e80d/Lib/heapq.py#l372_), for example if you're populating a list with huge number of items and cannot use a list-comprehension for some reason then caching list.append provides slight speedup:

>>> %%timeit
lst = []                  
for _ in xrange(10**6):
    lst.append(_)
... 
10 loops, best of 3: 47 ms per loop
>>> %%timeit
lst = [];append=lst.append
for _ in xrange(10**6):
    append(_)
... 
10 loops, best of 3: 31.3 ms per loop

Python 3.7

Python 3.7 will have two new byte codes to speed up method loading and calling.

Added two new opcodes: LOAD_METHOD and CALL_METHOD to avoid instantiation of bound method objects for method calls, which results in method calls being faster up to 20%. (Contributed by Yury Selivanov and INADA Naoki in bpo-26110.)

Loge answered 19/2, 2015 at 1:17 Comment(0)
H
1

You've run into scoping issue which is pretty detailed-ly explained here

Although scopes are determined statically, they are used dynamically. At any time during execution, there are at least three nested scopes whose namespaces are directly accessible:

  • the innermost scope, which is searched first, contains the local names
  • the scopes of any enclosing functions, which are searched starting with the nearest enclosing scope, contains non-local, but also non-global names
  • the next-to-last scope contains the current module’s global names
  • the outermost scope (searched last) is the namespace containing built-in names

So accessing local variables is 1 lookup less than instance variable and stacked against so many repetitions it works slower.

This question is also a possible duplicate of this one

Here answered 19/2, 2015 at 1:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.