Python函數局部變數如何執行?淺析python函數變數的應用

來源:互聯網
上載者:User
本篇文章給大家帶來的內容是關於Python函數局部變數如何執行?淺析python函數變數的應用 ,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所協助。

前言

這兩天在 CodeReview 時,看到這樣的代碼

# 虛擬碼import somelibclass 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 somelibclass 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: utf8a = 1def test(b):    c = 'test'        print a   # 全域變數    print b   # 局部變數    print c   # 局部變數test(3)
# 輸出13test
簡單來說,局部變數就是只作用於所在的函數域,超過範圍就被回收

理解了什麼是局部變數,就需要談談 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_functionx = fast_function(func, pp_stack, n, na, nk);

參數解釋下:

  1. func: 傳入的 test;

  2. pp_stack: 近似理解調用棧 (py方式);

  3. na: 位置參數個數;

  4. nk: 關鍵字個數;

  5. n = na + 2 * nk;

那麼下一步就看看 fast_function 要做什麼吧。

初始化一波

  1. 定義 co 來存放 test 對象裡面的 func_code

  2. 定義 globals 來存放 test 對象裡面的 func_globals (字典)

  3. 定義 argdefs 來存放 test 對象裡面的 func_defaults (構建函數時的關鍵字參數預設值)

來個判斷,如果 argdefs 為空白 && 傳入的位置參數個數 == 函數定義時候的位置形參個數 && 沒有傳入關鍵字參數

那就

  1. 當前線程狀態coglobals 來建立棧對象 f;

  2. 定義fastlocals ( fastlocals = f->f_localsplus; );

  3. 把 傳入的參數全部塞進去 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位元組。

到了這裡,棧對象 ff_localsplus 也登上曆史舞台了,只是此時的它,還只是一個未經人事的少年,還需曆練。

做好這些動作,終於來到真正執行函數的地方了: PyEval_EvalFrameEx,在這裡,需要先交代下,有個和 PyEval_EvalFrameEx 很像的,叫 PyEval_EvalCodeEx,雖然長得像,但是人家幹得活更多了。

請看回前面的 fast_function 開始那會有個判斷,我們上面說得是判斷成立的,也就是最簡單的函數執行情況。如果函數傳入多了關鍵字參數或者其他情況,那就複雜很多了,此時就需要由 PyEval_EvalCodeEx 處理一波,再執行 PyEval_EvalFrameEx

PyEval_EvalFrameEx 主要的工作就是解析位元組碼,像剛才的那些 CALL_FUNCTIONLOAD_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.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 timeitclass 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 = fucks = 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_ITERPOP_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 能夠稍微快點點點點點。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.