摘要:毫无疑问是全局,但是是什么鬼似乎应该叫吧然而事实就是这么神奇,人家就真的是叫,因为局部变量是从一个叫的数组里面读,故名字也这样取了。所以我们在一些会频繁操作类实例属性的情况下,应该是先把属性取出来存到局部变量,然后用局部变量来完成操作。
前言
这两天在 CodeReview 时,看到这样的代码
# 伪代码 import somelib class A(object): def load_project(self): self.project_code_to_name = {} for project in somelib.get_all_projects(): self.project_code_to_name[project] = project ...
意图很简单,就是将 somelib.get_all_projects 获取的项目塞入的 self.project_code_to_name
然而印象中这个是有优化空间的,于是提出调整方案:
import somelib class A(object): def load_project(self): project_code_to_name = {} for project in somelib.get_all_projects(): project_code_to_name[project] = project self.project_code_to_name = project_code_to_name ...
方案很简单,就是先定义局部变量 project_code_to_name,操作完,再赋值到self.project_code_to_name。
在后面的测试,也确实发现这样是会好点,那么结果知道了,接下来肯定是想探索原因的!
局部变量其实在网上很多地方,甚至很多书上都有讲过一个观点:访问局部变量速度要快很多,粗看好像好有道理,然后又看到下面贴了一大堆测试数据,虽然不知道是什么,但这是真的屌,记住再说,管他呢!
但是实际上这个观点还是有一定的局限性,并不是放诸四海皆准。所以先来理解下这句话吧,为什么大家都喜欢这样说。
先看段代码理解下什么是局部变量:
#coding: utf8 a = 1 def test(b): c = "test" print a # 全局变量 print b # 局部变量 print c # 局部变量 test(3)
# 输出 1 3 test
简单来说,局部变量就是只作用于所在的函数域,超过作用域就被回收
理解了什么是局部变量,就需要谈谈 Python 函数 和 局部变量 的爱恨情仇,因为如果不搞清楚这个,是很难感受到到底快在哪里;
为避免枯燥,以上述的代码来阐述吧,顺便附上 test 函数执行 的 dis 的解析:
# CALL_FUNCTION 5 0 LOAD_CONST 1 ("test") 3 STORE_FAST 1 (c) 6 6 LOAD_GLOBAL 0 (a) 9 PRINT_ITEM 10 PRINT_NEWLINE 7 11 LOAD_FAST 0 (b) 14 PRINT_ITEM 15 PRINT_NEWLINE 8 16 LOAD_FAST 1 (c) 19 PRINT_ITEM 20 PRINT_NEWLINE 21 LOAD_CONST 0 (None) 24 RETURN_VALUE
在上图中比较清楚能看到 a、b、c 分别对应的指令块,每一块的第一行都是 LOAD_XXX,顾名思义,是说明这些变量是从哪个地方获取的。
LOAD_GLOBAL 毫无疑问是全局,但是 LOAD_FAST 是什么鬼?似乎应该叫LOAD_LOCAL 吧?
然而事实就是这么神奇,人家就真的是叫 LOAD_FAST,因为局部变量是从一个叫 fastlocals 的数组里面读,故名字也这样取了。
那么是否存在这样的一个 LOAD_LOCAL?
答案是有的,不过人家不叫这个,而是叫LOAD_LOCALS,而且这个指令在这里却是完全不同的含义,为何?
因为这个指令几乎不会在函数运行出现,而是在类定义时才会出现(若其他同学发现其他场景也能看到这个,求分享):
# 测试代码 class A(object): s = 3
# 字节码
2 0 LOAD_CONST 0 ("A")
3 LOAD_NAME 0 (object)
6 BUILD_TUPLE 1
9 LOAD_CONST 1 ()
12 MAKE_FUNCTION 0
15 CALL_FUNCTION 0
18 BUILD_CLASS
19 STORE_NAME 1 (A)
22 LOAD_CONST 2 (None)
25 RETURN_VALUE
-------------------- 上面 CALL_FUNCTION 执行的内容如下 -------
2 0 LOAD_NAME 0 (__name__)
3 STORE_NAME 1 (__module__)
3 6 LOAD_CONST 0 (3)
9 STORE_NAME 2 (s)
12 LOAD_LOCALS
13 RETURN_VALUE
这里的 LOAD_NAME 和 STORE_NAME 打了一套组合拳,把 值 和 符号 关联了起来,并存到 f->f_locals
那么问题来了:f->f_locals是什么?怎么存?
这里的 f 就是一个帧对象,而 f_locals 是它的一个属性。而这个属性又比较神奇,在帧对象创建时,会被置为字典,而在函数机制内,又会被置为 NULL, 因为在函数机制内,就会用上面那套 fastlocals了。
那么在这里,就会引出一个小问题,有个叫 locals() 的函数,来打印局部变量,这又是怎么回事? 在另一篇文章已经谈到,欢迎移步: https://segmentfault.com/a/11...
接回上文,既然f->f_locals是字典,那就按照我们理解的字典那样存就好了呗~
这样就到了久违的LOAD_LOCALS了,具体实现:
TARGET_NOARG(LOAD_LOCALS) { if ((x = f->f_locals) != NULL) { Py_INCREF(x); PUSH(x); DISPATCH(); } PyErr_SetString(PyExc_SystemError, "no locals"); break; }
很通俗易懂,就是把刚才提到的、存了好多符号的 字典,拿出来塞到这个运行时栈 (下文会介绍到这个) 。
塞这个有啥用呢?这煞费苦心的一切,都是为了别人好啊!这种种的一切,都是为了 BUILD_CLASS 准备,因为需要利用这些来创建类!
那么关于类的知识,暂告一段落,下回再分解,咱们跑题都快跑出九霄凌外了
那么主角来了,我们要重点理解这个,因为这个确实还挺有意思。
Python 函数执行Python 函数的构建和运行,说复杂不复杂,说简单也不简单,因为它需要区分很多情况,比方说需要区分 函数 和 方法,再而区分是有无参数,有什么参数,有木有变长参数,有木有关键参数。
全部展开仔细讲是不可能的啦,不过可以简单图解下大致的流程(忽略参数变化细节):
一路顺流而下,直达 fast_function,它在这里的调用是:
// ceval.c -> call_function x = fast_function(func, pp_stack, n, na, nk);
参数解释下:
func: 传入的 test;
pp_stack: 近似理解调用栈 (py方式);
na: 位置参数个数;
nk: 关键字个数;
n = na + 2 * nk;
那么下一步就看看 fast_function 要做什么吧。
初始化一波
定义 co 来存放 test 对象里面的 func_code
定义 globals 来存放 test 对象里面的 func_globals (字典)
定义 argdefs 来存放 test 对象里面的 func_defaults (构建函数时的关键字参数默认值)
来个判断,如果 argdefs 为空 && 传入的位置参数个数 == 函数定义时候的位置形参个数 && 没有传入关键字参数
那就
用 当前线程状态、co 、globals 来新建栈对象 f;
定义fastlocals ( fastlocals = f->f_localsplus; );
把 传入的参数全部塞进去 fastlocals
那么问题来了,怎么塞?怎么找到传入了什么鬼参数:这个问题还是只能有 dis 来解答:
我们知道现在这步是在 CALL_FUNCTION 里面进行的,所以塞参数的动作,肯定是在此之前的,所以:
12 27 LOAD_NAME 2 (test) 30 LOAD_CONST 4 (3) 33 CALL_FUNCTION 1 36 POP_TOP 37 LOAD_CONST 1 (None) 40 RETURN_VALUE
在 CALL_FUNCTION 上面就看到 30 LOAD_CONST 4 (3),有兴趣的童鞋可以试下多传几个参数,就会发现传入的参数,是依次通过LOAD_CONST 这样的方式加载进来,所以如何找参数的问题就变得呼之欲出了;
// fast_function 函数 fastlocals = f->f_localsplus; stack = (*pp_stack) - n; for (i = 0; i < n; i++) { Py_INCREF(*stack); fastlocals[i] = *stack++; }
这里出现的 n 还记得怎么来的吗?回顾上面有个 n = na + 2 * nk; ,能想起什么吗?
其实这个地方就是简单的通过将 pp_stack 偏移 n 字节 找到一开始塞入参数的位置。
那么问题来了,如果 n 是 位置参数个数 + 关键字参数,那么 2 * nk 是什么意思?其实这答案很简单,那就是 关键字参数字节码 是属于带参数字节码, 是占 2字节。
到了这里,栈对象 f 的 f_localsplus 也登上历史舞台了,只是此时的它,还只是一个未经人事的少年,还需历练。
做好这些动作,终于来到真正执行函数的地方了: PyEval_EvalFrameEx,在这里,需要先交代下,有个和 PyEval_EvalFrameEx 很像的,叫 PyEval_EvalCodeEx,虽然长得像,但是人家干得活更多了。
请看回前面的 fast_function 开始那会有个判断,我们上面说得是判断成立的,也就是最简单的函数执行情况。如果函数传入多了关键字参数或者其他情况,那就复杂很多了,此时就需要由 PyEval_EvalCodeEx 处理一波,再执行 PyEval_EvalFrameEx。
PyEval_EvalFrameEx 主要的工作就是解析字节码,像刚才的那些 CALL_FUNCTION,LOAD_FAST 等等,都是由它解析和处理的,它的本质就是一个死循环,然后里面有一堆 swith - case,这基本也就是 Python 的运行本质了。
f_localsplus 存 和 取讲了这么长的一堆,算是把 Python 最基本的 函数调用过程简单扫了个盲,现在才开始探索主题。。
为了简单阐述,直接引用名词:fastlocals, 其中 fastlocals = f->f_localsplus
刚才只是简单看到了,Python 会把传入的参数,以此塞入 fastlocals 里面去,那么毋庸置疑,传入的位置参数,必然属于局部变量了,那么关键字参数呢?那肯定也是局部变量,因为它们都被特殊对待了嘛。
那么除了函数参数之外,必然还有函数内部的赋值咯? 这块字节码也一早在上面给出了:
# CALL_FUNCTION 5 0 LOAD_CONST 1 ("test") 3 STORE_FAST 1 (c)
这里出现了新的字节码 STORE_FAST,一起来看看实现把:
# PyEval_EvalFrameEx 庞大 switch-case 的其中一个分支: PREDICTED_WITH_ARG(STORE_FAST); TARGET(STORE_FAST) { v = POP(); SETLOCAL(oparg, v); FAST_DISPATCH(); } # 因为有涉及到宏,就顺便给出: #define GETLOCAL(i) (fastlocals[i]) #define SETLOCAL(i, value) do { PyObject *tmp = GETLOCAL(i); GETLOCAL(i) = value; Py_XDECREF(tmp); } while (0)
简单解释就是,将 POP() 获得的值 v,塞到 fastlocals 的 oparg 位置上。此处,v 是 "test", oparg 就是 1。用图表示就是:
有童鞋可能会突然懵了,为什么突然来了个 b ?我们又需要回到上面看 test 函数是怎样定义的:
// 我感觉往回看的概率超低的,直接给出算了 def test(b): c = "test" print b # 局部变量 print c # 局部变量
看到函数定义其实都应该知道了,因为 b 是传的参数啊,老早就塞进去了~
那存储知道了,那么怎么取呢?同样也是这段代码的字节码:
22 LOAD_FAST 1 (c)
虽然这个用脚趾头想想都知道原理是啥,但公平起见还是给出相应的代码:
# PyEval_EvalFrameEx 庞大 switch-case 的其中一个分支: TARGET(LOAD_FAST) { x = GETLOCAL(oparg); if (x != NULL) { Py_INCREF(x); PUSH(x); FAST_DISPATCH(); } format_exc_check_arg(PyExc_UnboundLocalError, UNBOUNDLOCAL_ERROR_MSG, PyTuple_GetItem(co->co_varnames, oparg)); break; }
直接用 GETLOCAL 通过索引在数组里取值了。
到了这里,应该也算是把 f_localsplus 讲明白了。这个地方不难,其实一般而言是不会被提及到这个,因为一般来说忽略即可了,但是如果说想在性能方面讲究点,那么这个小知识就不得忽视了。
变量使用姿势因为是面向对象,所以我们都习惯了通过 class 的方式,对于下面的使用方式,也是随手就来:
class SS(object): def __init__(self): self.test_dict = {} def test(self): print self.test_dict
这种方式一般是没什么问题的,也很规范。到那时如果是下面的操作,那就有问题了:
class SS(object): def __init__(self): self.test_dict = {} def test(self): num = 10 for i in range(num): self.test_dict[i] = i
这段代码的性能损耗,会随着 num 的值增大而增大, 如果下面循环中还要涉及到更多类属性的读取、修改等等,那影响就更大了
这个类属性如果换成 全局变量,也会存在类似的问题,只是说在操作类属性会比操作全局变量要频繁得多。
我们直接看看两者的差距有多大把?
import timeit class SS(object): def test(self): num = 100 self.test_dict = {} # 为了公平,每次执行都同样初始化新的 {} for i in range(num): self.test_dict[i] = i def test_local(self): num = 100 test_dict = {} # 为了公平,每次执行都同样初始化新的 {} for i in range(num): test_dict[i] = i self.test_dict = test_dict s = SS() print timeit.timeit(stmt=s.test_local) print timeit.timeit(stmt=s.test)
通过上图可以看出,随着 num 的值越大,for 循环的次数就越多,那么两者的差距也就越大了。
那么为什么会这样,也是在字节码可以看出写端倪:
// s.test >> 28 FOR_ITER 19 (to 50) 31 STORE_FAST 2 (i) 8 34 LOAD_FAST 2 (i) 37 LOAD_FAST 0 (self) 40 LOAD_ATTR 0 (test_dict) 43 LOAD_FAST 2 (i) 46 STORE_SUBSCR 47 JUMP_ABSOLUTE 28 >> 50 POP_BLOCK // s.test_local >> 25 FOR_ITER 16 (to 44) 28 STORE_FAST 3 (i) 14 31 LOAD_FAST 3 (i) 34 LOAD_FAST 2 (test_dict) 37 LOAD_FAST 3 (i) 40 STORE_SUBSCR 41 JUMP_ABSOLUTE 25 >> 44 POP_BLOCK 15 >> 45 LOAD_FAST 2 (test_dict) 48 LOAD_FAST 0 (self) 51 STORE_ATTR 1 (test_dict)
上面两段就是两个方法的 for block 内容,大家对比下就会知道, s.test 相比于 s.test_local, 多了个 LOAD_ATTR 放在 FOR_ITER 和 POP_BLOCK 之间。
这说明什么呢? 这说明,在每次循环时,s.test 都需要 LOAD_ATTR,很自然的,我们需要看看这个是干什么的:
TARGET(LOAD_ATTR) { w = GETITEM(names, oparg); v = TOP(); x = PyObject_GetAttr(v, w); Py_DECREF(v); SET_TOP(x); if (x != NULL) DISPATCH(); break; } # 相关宏定义 #define GETITEM(v, i) PyTuple_GetItem((v), (i))
这里出现了一个陌生的变量 name, 这是什么?其实这个就是每个 codeobject 所维护的一个 名字数组,基本上每个块所使用到的字符串,都会在这里面存着,同样也是有序的:
// PyCodeObject 结构体成员 PyObject *co_names; /* list of strings (names used) */
那么 LOAD_ATTR 的任务就很清晰了:先从名字列表里面取出字符串,结果就是 "hehe", 然后通过 PyObject_GetAttr 去查找,在这里就是在 s 实例中去查找。
且不说查找效率如何,光多了这一步,都能失之毫厘差之千里了,当然这是在频繁操作次数比较多的情况下。
所以我们在一些会频繁操作 类/实例属性 的情况下,应该是先把 属性 取出来存到 局部变量,然后用 局部变量 来完成操作。最后视情况把变动更新到 属性 上。
结语其实相比变量,在函数和方法的使用上面更有学问,更值得探索,因为那个原理和表面看起来差别更大,下次有机会再探讨。平时工作多注意下,才能使得我们的 PY 能够稍微快点点点点点。
欢迎各位大神指点交流, QQ讨论群: 258498217
转载请注明来源: https://segmentfault.com/a/11...
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/42334.html
python在比较新的版本,3.7这个版本中,引入了一个比较新的模块contextvars,从名字上来看的话,它是形容为上下变量的,下文就给大家详细的解答下,关于这方面的内容。 Python在3.7的时候引入了一个模块:contextvars,从名字上很容易看出它指的是上下文变量(Context Variables),所以在介绍contextvars之前我们需要先了解一下什么是上下文(Con...
本文关键阐述了处理python递归函数及递归算法频次受限制难题,具有非常好的实用价值,希望能帮助到大家。如有误或者未考虑到真正的地区,望鼎力相助 递归函数及递归算法频次受限制 一个函数在外部启用自身,那么这样的函数是递归函数。递归算法要反复应用自身,每递归算法一回,越近最后的值。如果一个难题需要由很多类似小问题处理,可以选择应用递归函数。伴随着递归算法的深层次,难题经营规模对比之前都应该所...
本文不会过多讲解基础知识,更多说的是在使用useRef如何能摆脱 这个 闭包陷阱 ? react hooks 的闭包陷阱 基本每个开发员都有遇见,这是很令人抓狂的。 (以下react示范demo,均为react 16.8.3 版本) 列一个具体的场景: functionApp(){ const[count,setCount]=useState(1); useEffect(()=...
小编写这篇文章的主要目的,就是给大家介绍关于python装饰器的用途方面的知识,这样对我们以后的工作也是有一定的帮助,具体用途是什么呢?下面就给各位读者详细的解答下。 装饰器自身的价值显而易见,可以加强函数值基本功能、简单化编码、降低代码冗余。 它的使用场景同样很多,比较简单的场景包含打印日志、统计运行时间,这类例子和用法网上已经很多了: deftime_dec(func): def...
小编写这篇文章的主要目的,主要是给大家介绍关于python数字图像处理的一些相关介绍,包括基本的形态学滤波,比如可以使用数字图像去进行处理类似的形态学滤波。那么,具体的内容呢?下面就给大家详细解答下。 引言 对图像进行形态学变换。变换对象一般为灰度图或二值图,功能函数放在morphology子模块内。 1、膨胀(dilation) 原理:一般对二值图像进行操作。找到像素值为1的点,将它...
阅读 3317·2019-08-29 16:17
阅读 1971·2019-08-29 15:31
阅读 2644·2019-08-29 14:09
阅读 2547·2019-08-26 13:52
阅读 743·2019-08-26 12:21
阅读 2124·2019-08-26 12:08
阅读 990·2019-08-23 17:08
阅读 1922·2019-08-23 16:59