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

问题描述 投票:0回答:3
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

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

python instance-variables
3个回答
13
投票

每次 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 反汇编字节码,你也可以看到为

f2
所做的事情比为
f1
做的更多。

另请注意,并非所有操作码都是相同的。

LOAD_FAST
基本上是本地范围内的数组查找(因此顾名思义,它是快速的)。
LOAD_ATTR
(另一方面)有点慢,因为它转换为(通常)进行字典查找的函数调用(
__getattribute__
)。


就什么是“最佳实践”而言,做最简单的事情。 我认为使用

self
是很习惯的,除非你证明避免它可以显着提高性能,但我不认为这是一个硬性规则。


10
投票

因为局部变量只需使用单个字节代码步骤访问

LOAD_FAST
,另一方面,
self.x
将需要首先使用
self
查找
LOAD_FAST
,然后访问它的
x
,也是如此很复杂,因为 Python 必须首先检查它是数据描述符还是简单的实例属性,然后基于此来获取它的值。

通常,在 CPython 中处理 methods 时,缓存如此频繁重复的调用是个好主意,因为否则每次都会创建一个新的绑定对象。我几乎没有见过缓存普通属性以获得一些性能优势的情况。其他实现如 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

Python 3.7

Python 3.7 将有两个新的字节码以加快方法加载和调用。

添加了两个新的操作码:

LOAD_METHOD
CALL_METHOD
以避免 用于方法调用的绑定方法对象的实例化,其结果 方法调用速度加快 20%。 (尤里·塞利瓦诺夫供稿) 和 INADA Naoki 在 bpo-26110。)


1
投票

您遇到了范围界定问题,该问题在here

有非常详细的解释

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

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

因此访问局部变量比实例变量少 1 次查找 如果重复次数太多,效果就会变慢。

这个问题也可能与这个问题

重复
最新问题
© www.soinside.com 2019 - 2025. All rights reserved.