日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > python >内容正文

python

python函数调用位置_Python: 浅谈函数局部变量快在哪

發(fā)布時間:2024/4/19 python 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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

然而印象中這個是有優(yōu)化空間的,于是提出調整方案:

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。

在后面的測試,也確實發(fā)現這樣是會好點,那么結果知道了,接下來肯定是想探索原因的!

局部變量

其實在網上很多地方,甚至很多書上都有講過一個觀點:訪問局部變量速度要快很多,粗看好像好有道理,然后又看到下面貼了一大堆測試數據,雖然不知道是什么,但這是真的屌,記住再說,管他呢!

但是實際上這個觀點還是有一定的局限性,并不是放諸四海皆準。所以先來理解下這句話吧,為什么大家都喜歡這樣說。

先看段代碼理解下什么是局部變量:

#coding: utf8

a = 1

def test(b):

c = 'test'

print a # 全局變量

print b # 局部變量

print c # 局部變量

test(3)

# 輸出

1

3

test

簡單來說,局部變量就是只作用于所在的函數域,超過作用域就被回收

理解了什么是局部變量,就需要談談 Python 函數 和 局部變量 的愛恨情仇,因為如果不搞清楚這個,是很難感受到到底快在哪里;

為避免枯燥,以上述的代碼來闡述吧,順便附上 test 函數執(zhí)行 的 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,而且這個指令在這里卻是完全不同的含義,為何?

因為這個指令幾乎不會在函數運行出現,而是在類定義時才會出現(若其他同學發(fā)現其他場景也能看到這個,求分享):

# 測試代碼

class A(object):

s = 3

# 字節(jié)碼

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 執(zhí)行的內容如下 -------

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 是它的一個屬性。而這個屬性又比較神奇,在幀對象創(chuàng)建時,會被置為字典,而在函數機制內,又會被置為 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 準備,因為需要利用這些來創(chuàng)建類!

那么關于類的知識,暫告一段落,下回再分解,咱們跑題都快跑出九霄凌外了

那么主角來了,我們要重點理解這個,因為這個確實還挺有意思。

Python 函數執(zhí)行

Python 函數的構建和運行,說復雜不復雜,說簡單也不簡單,因為它需要區(qū)分很多情況,比方說需要區(qū)分 函數 和 方法,再而區(qū)分是有無參數,有什么參數,有木有變長參數,有木有關鍵參數。

全部展開仔細講是不可能的啦,不過可以簡單圖解下大致的流程(忽略參數變化細節(jié)):

一路順流而下,直達 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 為空 && 傳入的位置參數個數 == 函數定義時候的位置形參個數 && 沒有傳入關鍵字參數

那就

用 當前線程狀態(tài)、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),有興趣的童鞋可以試下多傳幾個參數,就會發(fā)現傳入的參數,是依次通過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 字節(jié) 找到一開始塞入參數的位置。

那么問題來了,如果 n 是 位置參數個數 + 關鍵字參數,那么 2 * nk 是什么意思?其實這答案很簡單,那就是 關鍵字參數字節(jié)碼 是屬于帶參數字節(jié)碼, 是占 2字節(jié)。

到了這里,棧對象 f 的 f_localsplus 也登上歷史舞臺了,只是此時的它,還只是一個未經人事的少年,還需歷練。

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

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

PyEval_EvalFrameEx 主要的工作就是解析字節(jié)碼,像剛才的那些 CALL_FUNCTION,LOAD_FAST 等等,都是由它解析和處理的,它的本質就是一個死循環(huán),然后里面有一堆 swith - case,這基本也就是 Python 的運行本質了。

f_localsplus 存 和 取

講了這么長的一堆,算是把 Python 最基本的 函數調用過程簡單掃了個盲,現在才開始探索主題。。

為了簡單闡述,直接引用名詞:fastlocals, 其中 fastlocals = f->f_localsplus

剛才只是簡單看到了,Python 會把傳入的參數,以此塞入 fastlocals 里面去,那么毋庸置疑,傳入的位置參數,必然屬于局部變量了,那么關鍵字參數呢?那肯定也是局部變量,因為它們都被特殊對待了嘛。

那么除了函數參數之外,必然還有函數內部的賦值咯? 這塊字節(jié)碼也一早在上面給出了:

# CALL_FUNCTION

5 0 LOAD_CONST 1 ('test')

3 STORE_FAST 1 (c)

這里出現了新的字節(jié)碼 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 是傳的參數啊,老早就塞進去了~

那存儲知道了,那么怎么取呢?同樣也是這段代碼的字節(jié)碼:

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

這種方式一般是沒什么問題的,也很規(guī)范。到那時如果是下面的操作,那就有問題了:

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 的值增大而增大, 如果下面循環(huán)中還要涉及到更多類屬性的讀取、修改等等,那影響就更大了

這個類屬性如果換成 全局變量,也會存在類似的問題,只是說在操作類屬性會比操作全局變量要頻繁得多。

我們直接看看兩者的差距有多大把?

import timeit

class SS(object):

def test(self):

num = 100

self.test_dict = {} # 為了公平,每次執(zhí)行都同樣初始化新的 {}

for i in range(num):

self.test_dict[i] = i

def test_local(self):

num = 100

test_dict = {} # 為了公平,每次執(zhí)行都同樣初始化新的 {}

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 循環(huán)的次數就越多,那么兩者的差距也就越大了。

那么為什么會這樣,也是在字節(jié)碼可以看出寫端倪:

// 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 之間。

這說明什么呢? 這說明,在每次循環(huán)時,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

總結

以上是生活随笔為你收集整理的python函数调用位置_Python: 浅谈函数局部变量快在哪的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。