Python |为什么访问实例属性比本地慢?

时间:2015-02-19 01:07:13

标签: python instance-variables

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)))

我的电脑的结果:

访问本地属性:0.686281020000024

访问实例属性:3.7962001440000677

为什么会这样?此外,在使用之前本地化实例变量是一个好习惯吗?

3 个答案:

答案 0 :(得分:10)

每次python查找变量时,都会支付一点(LOAD_FAST操作码)。每次在现有对象上查找属性时,都需要多付一些(LOAD_ATTR操作码)。 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        
>>> 

即使您不知道如何阅读python反汇编字节码,您也可以看到f2的内容比f1更多。

另外,请注意并非所有操作码都相同。 LOAD_FAST基本上是本地范围内的数组查找(因此顾名思义,它是快速的)。另一方面,LOAD_ATTR稍微慢一点,因为它转换为(通常)进行字典查找的函数调用(__getattribute__)。


就什么是“最佳实践”而言,做最简单的事情。我认为使用self是非常习惯的,除非你通过避免它来证明有明显的性能提升,但我不认为这是一个很难的规则。

答案 1 :(得分:6)

由于只使用单字节代码步骤LOAD_FAST访问局部变量,另一方面self.x需要首先使用self查找LOAD_FAST然后访问x就此而言,这也很复杂,因为Python必须首先检查它是数据描述符还是仅仅是简单的实例属性,然后根据它来获取它的值。

通常在CPython中处理方法时缓存这种重复调用是个好主意,因为否则每次创建新的绑定对象时都是如此。我很难看到普通属性被缓存以获得一些性能优势的情况。像PyPyPyston这样的其他实现有自己的方法来加速属性查找。来自data model页面:

  

请注意,从函数对象转换为(未绑定或   每次从中检索属性时都会发生方法对象   类或实例。在某些情况下,卓有成效的优化是   将属性分配给局部变量并调用该局部变量。

某些例子是list.append(另请参阅:https://hg.python.org/cpython/file/f7fd2776e80d/Lib/heapq.py#l372_),例如,如果您要填充包含大量项目的列表,并且不能对某些项目使用列表理解原因然后缓存list.append提供了轻微的加速:

>>> %%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将有two new byte codes来加速方法加载和调用。

  

添加了两个新的操作码:LOAD_METHODCALL_METHOD以避免   方法调用的绑定方法对象的实例化,结果   方法调用速度最快可达20%。 (供稿人:Yury Selivanov   和{INNA Naoki在bpo-26110。)

答案 2 :(得分:1)

您已经遇到了范围问题,这是一个非常详细的解释here

  

尽管范围是静态确定的,但它们是动态使用的。在执行期间的任何时候,至少有三个嵌套的作用域,其名称空间可以直接访问:

     
      
  • 首先搜索的最里面的范围包含本地名称
  •   
  • 任何封闭函数的范围,从最近的封闭范围开始搜索,包含非本地,但也   非全球名称
  •   
  • 倒数第二个范围包含当前模块的全局名称
  •   
  • 最外层范围(最后搜索)是包含内置名称的命名空间
  •   

因此访问局部变量比实例变量少1次查找 反对如此多的重复堆叠,它工作得更慢。

此问题也可能与this one

重复