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

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

我电脑的结果:

访问本地属性:0.686281020000024

访问实例属性:3.7962001440000677

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

您 运行 进入了范围界定问题,该问题的解释非常详细 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

所以访问局部变量比实例变量少了1次查找 并与如此多的重复堆叠在一起,它的工作速度变慢了。

这个问题也可能与 this one

重复

每次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        
>>> 

即使您不知道如何阅读 python 反汇编字节码,您也可以看到 f2f1.[=20 完成了更多的工作=]

另外,请注意并非所有操作码都相同。 LOAD_FAST is basically a array lookup in the local scope (so it is FAST as the name implies). LOAD_ATTR(另一方面)转换为函数调用(__getattribute__)时(通常)进行字典查找,因此速度稍慢。


至于什么是"best practice",做最简单的阅读。我认为使用 self 是很常见的做法,除非您证明通过避免它可以显着提高性能,但我认为这不是硬性规定。

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

通常,在处理 CPython 中的 方法 时,缓存如此频繁重复的调用是个好主意,否则每次都会创建一个新的绑定对象。我几乎没有见过缓存普通属性以获得一些性能优势的情况。其他实现如 PyPy and Pyston have their own way of speeding up attribute lookups. From data model 页面:

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.

一个例子是 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加快方法加载和调用速度

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