我对Python还不是很熟悉,一直在尝试提高我的Python脚本的性能,所以我测试了使用全局变量和将本地变量传递给函数两种情况。我计时后发现,令我惊讶的是,在声明全局变量后脚本运行得比传递本地变量更快。这是怎么回事?我认为使用本地变量执行速度更快?(我知道全局变量不安全,但我还是很好奇。)
我对Python还不是很熟悉,一直在尝试提高我的Python脚本的性能,所以我测试了使用全局变量和将本地变量传递给函数两种情况。我计时后发现,令我惊讶的是,在声明全局变量后脚本运行得比传递本地变量更快。这是怎么回事?我认为使用本地变量执行速度更快?(我知道全局变量不安全,但我还是很好奇。)
当一行代码要求变量x的值时,Python将按顺序在所有可用的命名空间中搜索该变量:
- 本地命名空间 - 特定于当前函数或类方法。如果函数定义了一个本地变量x,或者有一个参数x,Python将使用它并停止搜索。
- 全局命名空间 - 特定于当前模块。如果模块定义了一个名为x的变量、函数或类,Python将使用它并停止搜索。
- 内置命名空间 - 对所有模块都是全局的。作为最后的选择,Python将假定x是内置函数或变量的名称。
基于此,我认为本地变量通常会更快。我的猜测是,你看到的是你的脚本的特殊情况。
以下是一个使用本地变量的微不足道的示例,在我的机器上大约需要0.5秒(在Python 3中为0.3秒):
def func():
for i in range(10000000):
x = 5
func()
全球版本大约需要0.7秒(Python 3中为0.5秒):
def func():
global x
for i in range(1000000):
x = 5
func()
global
会对已经是全局变量的变量产生奇怪的影响有趣的是,这个版本运行时间为0.8秒:
global x
x = 5
for i in range(10000000):
x = 5
虽然这在0.9中运行:
x = 5
for i in range(10000000):
x = 5
x
是一个全局变量(因为没有函数),并且它们都比使用本地变量慢。我不知道为什么在这种情况下声明global x
有所帮助。简单回答:
由于Python的动态性,在遇到像a.b.c这样的表达式时,解释器会首先查找a(先在本地命名空间中查找,然后是全局命名空间,最后是内置命名空间),然后它会在该对象的命名空间中查找以解析名称b,最后它会在该对象的命名空间中查找以解析名称c。这些查找非常快速;对于局部变量,查找非常快,因为解释器知道哪些变量是局部的,并且可以将它们分配到内存中已知的位置。
解释器知道函数内部的哪些名称是局部的,并将它们指定为函数调用内存中的特定(已知)位置。这使得对局部变量的引用比对全局变量和内置变量的引用快得多。
代码示例以说明此原理:
>>> glen = len # provides a global reference to a built-in
>>>
>>> def flocal():
... name = len
... for i in range(25):
... x = name
...
>>> def fglobal():
... for i in range(25):
... x = glen
...
>>> def fbuiltin():
... for i in range(25):
... x = len
...
>>> timeit("flocal()", "from __main__ import flocal")
1.743438959121704
>>> timeit("fglobal()", "from __main__ import fglobal")
2.192162036895752
>>> timeit("fbuiltin()", "from __main__ import fbuiltin")
2.259413003921509
>>>
当Python编译函数时,函数知道在调用它之前其中的变量是局部变量、闭包还是全局变量。
我们有几种方法可以在函数中引用变量:
因此,让我们在几个不同的函数中创建这些类型的变量,以便我们自己可以看到:
global_foo = 'foo'
def globalfoo():
return global_foo
def makeclosurefoo():
boundfoo = 'foo'
def innerfoo():
return boundfoo
return innerfoo
closurefoo = makeclosurefoo()
def defaultfoo(foo='foo'):
return foo
def localfoo():
foo = 'foo'
return foo
我们可以看到每个函数都知道在哪里查找变量 - 它不需要在运行时这样做:
>>> import dis
>>> dis.dis(globalfoo)
2 0 LOAD_GLOBAL 0 (global_foo)
2 RETURN_VALUE
>>> dis.dis(closurefoo)
4 0 LOAD_DEREF 0 (boundfoo)
2 RETURN_VALUE
>>> dis.dis(defaultfoo)
2 0 LOAD_FAST 0 (foo)
2 RETURN_VALUE
>>> dis.dis(localfoo)
2 0 LOAD_CONST 1 ('foo')
2 STORE_FAST 0 (foo)
3 4 LOAD_FAST 0 (foo)
6 RETURN_VALUE
我们可以看到,当前全局变量的字节码为LOAD_GLOBAL
,闭包变量为LOAD_DEREF
,局部变量为LOAD_FAST
。这些是CPython的实现细节,可能会因版本而异,但能够看到Python对每个变量查找的处理方式是有用的。
将其粘贴到解释器中并自行查看:
import dis
dis.dis(globalfoo)
dis.dis(closurefoo)
dis.dis(defaultfoo)
dis.dis(localfoo)
测试代码(请放心在您的系统上进行测试):
import sys
sys.version
import timeit
min(timeit.repeat(globalfoo))
min(timeit.repeat(closurefoo))
min(timeit.repeat(defaultfoo))
min(timeit.repeat(localfoo))
在Windows上,至少在此版本中,闭包似乎会有一些惩罚 - 使用默认值的本地变量是最快的,因为您不必每次都分配该变量:
>>> import sys
>>> sys.version
'3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]'
>>> import timeit
>>> min(timeit.repeat(globalfoo))
0.0728403456180331
>>> min(timeit.repeat(closurefoo))
0.07465484920749077
>>> min(timeit.repeat(defaultfoo))
0.06542038103088998
>>> min(timeit.repeat(localfoo))
0.06801849537714588
在Linux上:
>>> import sys
>>> sys.version
'3.6.4 |Anaconda custom (64-bit)| (default, Mar 13 2018, 01:15:57) \n[GCC 7.2.0]'
>>> import timeit
>>> min(timeit.repeat(globalfoo))
0.08560040907468647
>>> min(timeit.repeat(closurefoo))
0.08592104795388877
>>> min(timeit.repeat(defaultfoo))
0.06587386003229767
>>> min(timeit.repeat(localfoo))
0.06887826602905989
我在有机会测试其他系统时会将它们添加进来。