本篇文章帶給大家的內容是關於Python函數局部變數如何執行?淺析python函數變數的應用 ,有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。
前言
這兩天在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
的陣列裡面讀,所以名字也就這樣叫了(我猜的)。
那麼主角來了,我們要重點理解這個,因為這個確實還蠻有趣。
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
func_globals (字典)
func_defaults (建構函式時的關鍵字參數預設值)
argdefs 為空 &&
傳入的位置參數數位== 函數定義時候的位置形參個數 &&
沒有傳入關鍵字參數
目前執行緒狀態、
co 、
globals 來新堆疊物件
f;
fastlocals ( fastlocals = f- >f_localsplus; );
fastlocals
來解答:我們知道現在這步是在
裡面進行的,所以塞參數的動作,肯定是在此之前的,所以:<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false"> 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</pre><div class="contentsignin">登入後複製</div></div>
在
上面就看到 30 LOAD_CONST 4 (3)
,有興趣的童鞋##可以試下多傳幾個參數,就會發現傳入的參數,是依序透過LOAD_CONST
這樣的方式載入進來,所以如何找參數的問題就變得呼之欲出了;<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">// fast_function 函数
fastlocals = f->f_localsplus;
stack = (*pp_stack) - n;
for (i = 0; i <p>这里出现的 n 还记得怎么来的吗?回顾上面有个 <code>n = na + 2 * nk;</code> ,能想起什么吗?</p><p>其实这个地方就是简单的通过将 <code>pp_stack</code> 偏移 n 字节 找到一开始塞入参数的位置。</p><p>那么问题来了,如果 n 是 位置参数个数 + 关键字参数,那么 2 * nk 是什么意思?其实这答案很简单,那就是 关键字参数字节码 是属于带参数字节码, 是占 2字节。</p><p>到了这里,栈对象 <code>f</code> 的 <code>f_localsplus</code> 也登上历史舞台了,只是此时的它,还只是一个未经人事的少年,还需历练。</p><p>做好这些动作,终于来到真正执行函数的地方了: <code>PyEval_EvalFrameEx</code>,在这里,需要先交代下,有个和 <code>PyEval_EvalFrameEx</code> 很像的,叫 <code>PyEval_EvalCodeEx</code>,虽然长得像,但是人家干得活更多了。</p><p>请看回前面的 <code>fast_function</code> 开始那会有个判断,我们上面说得是判断成立的,也就是最简单的函数执行情况。如果函数传入多了关键字参数或者其他情况,那就复杂很多了,此时就需要由 <code>PyEval_EvalCodeEx</code> 处理一波,再执行 <code>PyEval_EvalFrameEx</code>。</p><p><code>PyEval_EvalFrameEx</code> 主要的工作就是解析字节码,像刚才的那些 <code>CALL_FUNCTION</code>,<code>LOAD_FAST</code> 等等,都是由它解析和处理的,它的本质就是一个死循环,然后里面有一堆 <code>swith - case</code>,这基本也就是 Python 的运行本质了。</p><h4>f_localsplus 存 和 取</h4><p>讲了这么长的一堆,算是把 Python 最基本的 函数调用过程简单扫了个盲,现在才开始探索主题。。</p><p>为了简单阐述,直接引用名词:<code>fastlocals</code>, 其中 <code>fastlocals = f->f_localsplus</code></p><p>刚才只是简单看到了,Python 会把传入的参数,以此塞入 <code>fastlocals</code> 里面去,那么毋庸置疑,传入的位置参数,必然属于局部变量了,那么关键字参数呢?那肯定也是局部变量,因为它们都被特殊对待了嘛。</p><p>那么除了函数参数之外,必然还有函数内部的赋值咯? 这块字节码也一早在上面给出了:</p><pre class="brush:php;toolbar:false"># CALL_FUNCTION
5 0 LOAD_CONST 1 ('test')
3 STORE_FAST 1 (c)</pre><div class="contentsignin">登入後複製</div></div><p>这里出现了新的字节码 <code>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.fuck = {} def test(self): print self.fuck
这种方式一般是没什么问题的,也很规范。到那时如果是下面的操作,那就有问题了:
class SS(object): def __init__(self): self.fuck = {} def test(self): num = 10 for i in range(num): self.fuck[i] = i
这段代码的性能损耗,会随着 num 的值增大而增大, 如果下面循环中还要涉及到更多类属性的读取、修改等等,那影响就更大了
这个类属性如果换成 全局变量,也会存在类似的问题,只是说在操作类属性会比操作全局变量要频繁得多。
我们直接看看两者的差距有多大把?
import timeit class SS(object): def test(self): num = 100 self.fuck = {} # 为了公平,每次执行都同样初始化新的 {} for i in range(num): self.fuck[i] = i def test_local(self): num = 100 fuck = {} # 为了公平,每次执行都同样初始化新的 {} for i in range(num): fuck[i] = i self.fuck = fuck 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 (hehe) 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 (hehe) 37 LOAD_FAST 3 (i) 40 STORE_SUBSCR 41 JUMP_ABSOLUTE 25 >> 44 POP_BLOCK 15 >> 45 LOAD_FAST 2 (hehe) 48 LOAD_FAST 0 (self) 51 STORE_ATTR 1 (hehe)
上面两段就是两个方法的 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 能够稍微快点点点点点。
相关推荐:
以上是Python函數局部變數如何執行?淺析python函數變數的應用的詳細內容。更多資訊請關注PHP中文網其他相關文章!