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

han*_*s-t 6 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)))
Run Code Online (Sandbox Code Playgroud)

我的电脑的结果:

访问本地属性:0.686281020000024

访问实例属性:3.7962001440000677

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

mgi*_*son 10

每次python查找变量时,都需要支付一些费用(LOAD_FAST操作代码).每次在现有对象上查找属性时,都需要支付更多费用(LOAD_ATTR操作代码).例如

>>> 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        
>>> 
Run Code Online (Sandbox Code Playgroud)

即使你不知道如何阅读python反汇编字节码,你也可以看到有更多东西要做,而f2不是f1.

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


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


Ash*_*ary 6

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

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

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

这方面的一个例子是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
Run Code Online (Sandbox Code Playgroud)

Python 3.7

Python 3.7将有两个新的字节代码来加速方法加载和调用.

添加了两个新的操作码:LOAD_METHODCALL_METHOD避免实例化方法调用的绑定方法对象,这会导致方法调用速度提高20%.(由Yury Selivanov和INADA Naoki 供稿bpo-26110.)