最新文章專題視頻專題問答1問答10問答100問答1000問答2000關(guān)鍵字專題1關(guān)鍵字專題50關(guān)鍵字專題500關(guān)鍵字專題1500TAG最新視頻文章推薦1 推薦3 推薦5 推薦7 推薦9 推薦11 推薦13 推薦15 推薦17 推薦19 推薦21 推薦23 推薦25 推薦27 推薦29 推薦31 推薦33 推薦35 推薦37視頻文章20視頻文章30視頻文章40視頻文章50視頻文章60 視頻文章70視頻文章80視頻文章90視頻文章100視頻文章120視頻文章140 視頻2關(guān)鍵字專題關(guān)鍵字專題tag2tag3文章專題文章專題2文章索引1文章索引2文章索引3文章索引4文章索引5123456789101112131415文章專題3
問答文章1 問答文章501 問答文章1001 問答文章1501 問答文章2001 問答文章2501 問答文章3001 問答文章3501 問答文章4001 問答文章4501 問答文章5001 問答文章5501 問答文章6001 問答文章6501 問答文章7001 問答文章7501 問答文章8001 問答文章8501 問答文章9001 問答文章9501
當(dāng)前位置: 首頁 - 科技 - 知識百科 - 正文

講解Python中for循環(huán)下的索引變量的作用域

來源:懂視網(wǎng) 責(zé)編:小采 時(shí)間:2020-11-27 14:32:38
文檔

講解Python中for循環(huán)下的索引變量的作用域

講解Python中for循環(huán)下的索引變量的作用域:我們從一個(gè)測試開始。下面這個(gè)函數(shù)的功能是什么? def foo(lst): a = 0 for i in lst: a += i b = 1 for t in lst: b *= i return a, b 如果你覺得它的功能是計(jì)算lst中所有元素的和與積,不要沮喪。通常很難發(fā)現(xiàn)這里的錯(cuò)誤。如果在大堆真實(shí)的代
推薦度:
導(dǎo)讀講解Python中for循環(huán)下的索引變量的作用域:我們從一個(gè)測試開始。下面這個(gè)函數(shù)的功能是什么? def foo(lst): a = 0 for i in lst: a += i b = 1 for t in lst: b *= i return a, b 如果你覺得它的功能是計(jì)算lst中所有元素的和與積,不要沮喪。通常很難發(fā)現(xiàn)這里的錯(cuò)誤。如果在大堆真實(shí)的代

我們從一個(gè)測試開始。下面這個(gè)函數(shù)的功能是什么?

def foo(lst):
 a = 0
 for i in lst:
 a += i
 b = 1
 for t in lst:
 b *= i
 return a, b

如果你覺得它的功能是“計(jì)算lst中所有元素的和與積”,不要沮喪。通常很難發(fā)現(xiàn)這里的錯(cuò)誤。如果在大堆真實(shí)的代碼中發(fā)現(xiàn)了這個(gè)錯(cuò)誤就非常厲害了?!?dāng)你不知道這是一個(gè)測試時(shí),很難發(fā)現(xiàn)這個(gè)錯(cuò)誤。

這里的錯(cuò)誤是在第二個(gè)循環(huán)體中使用了i而不是t。等下,這到底是怎么工作的?i在第一個(gè)循環(huán)外應(yīng)該是不可見的? [1]哦,不。事實(shí)上,Python正式聲明過,為for循環(huán)目標(biāo)(loop target)定義的名稱(更嚴(yán)格的正式名稱為“索引變量”)能泄露到外圍函數(shù)范圍。因此下面的代碼:

for i in [1, 2, 3]:
 pass
print(i)

這段代碼是有效的,可以打印出3。在本文中,我想探討一下為什么會(huì)這樣,為什么它不太可能改變,以及將它作為一顆追蹤子彈來挖掘CPython編輯器中一些有趣的部分。

順便說一句,如果你不相信這種行為可能會(huì)導(dǎo)致真正的問題,考慮這個(gè)代碼片斷:

def foo():
 lst = []
 for i in range(4):
 lst.append(lambda: i)
 print([f() for f in lst])

如果你期待上面的代碼能打印出[0,1,2,3],你的期望會(huì)落空的,它會(huì)打印出[3,3,3,3];因?yàn)樵趂oo的作用域內(nèi)只有一個(gè)i,這個(gè)i就是所有的lambda所捕獲的。
官方說明

Python參考文檔中的for循環(huán)部分明確地記錄了這種行為:

for循環(huán)將變量賦值到目標(biāo)列表中?!?dāng)循環(huán)結(jié)束時(shí),賦值列表中的變量不會(huì)被刪除,但如果序列是空的,它們將不會(huì)被賦值給所有的循環(huán)。

注意最后一句,讓我們試試:

for i in []:
 pass
print(i)

的確,上面的代碼拋出NameError異常。稍后,我們將看到這是Python虛擬機(jī)執(zhí)行字節(jié)碼方式的必然結(jié)果。
為什么會(huì)是這樣

其實(shí)我問過Guido van Rossum有關(guān)這個(gè)執(zhí)行行為的原因,他很慷慨地告訴了我其中的一些歷史背景(感謝Guido?。?。這樣執(zhí)行代碼的動(dòng)機(jī)是保持Python獲得變量和作用域的簡單性,而不訴諸于hacks(例如在循環(huán)完成后,刪除定義在該循環(huán)中的所有變量——想想它可能引發(fā)的異常)或更復(fù)雜的作用域規(guī)則。

Python的作用域規(guī)則非常簡單、優(yōu)雅:模塊、類以及函數(shù)的代碼塊可引入作用域。在函數(shù)體內(nèi),變量從它們定義到代碼塊結(jié)束(包括嵌套的代碼塊如嵌套函數(shù))都是可見的。當(dāng)然,對于局部變量、全局變量(以及其他nonlocal變量)其規(guī)則略有不同。不過,這和我們的討論沒有太多關(guān)系。

這里最重要的一點(diǎn)是:最內(nèi)層的可能作用域是一個(gè)函數(shù)體。不是一個(gè)for循環(huán)體。不是一個(gè)with代碼塊。Python與其他編程語言不同(例如C及其后代語言),在函數(shù)水平下沒有嵌套詞法作用域。

因此,如果你只是基于Python實(shí)現(xiàn),你的代碼可能會(huì)以這樣的執(zhí)行行為結(jié)束。下面是另一段令人啟發(fā)的代碼片段:

for i in range(4):
 d = i * 2
print(d)

變量d 在for循環(huán)結(jié)束后是可見及可訪問的,你對這樣的發(fā)現(xiàn)感到驚奇嗎?不,這正是Python的工作方式。那么,為什么索引變量的作用域被區(qū)別對待呢?

順便說一句,列表推導(dǎo)式(list comprehension)中的索引變量也泄露到其封閉作用域,或者更準(zhǔn)確的說,在Python 3之前可以泄露。

Python 3包含許多重大更改,其中也修復(fù)了列表推導(dǎo)式中的變量泄露問題。毫無疑問,這樣破壞了向后兼容中性。這就是我認(rèn)為當(dāng)前的執(zhí)行行為不會(huì)被改變的原因。

此外,許多人仍然發(fā)現(xiàn)這是Python中的一個(gè)有用的功能??紤]一下下面的代碼:

for i, item in enumerate(somegenerator()):
 dostuffwith(i, item)
print('The loop executed {0} times!'.format(i+1))

如果不知道somegenerator返回項(xiàng)的數(shù)目,可以使用這種簡潔的方式。否則,你就必須有一個(gè)獨(dú)立的計(jì)數(shù)器。

這里有一個(gè)其他的例子:

for i in somegenerator():
 if isinteresing(i):
 break
dostuffwith(i)

這種模式可以有效的在循環(huán)中查找某一項(xiàng)并在隨后使用該項(xiàng)。[2]

多年來,許多用戶都想保留這種特性。但即使對于開發(fā)者認(rèn)定的有害特性,也很難引入重大更改了。當(dāng)許多人認(rèn)為該特性很有用,而且在真實(shí)世界的代碼中大量使用時(shí),就更不會(huì)除去這項(xiàng)特性了。
Under the hood

現(xiàn)在是最有趣的部分。讓我們來看看Python編譯器和VM是如何協(xié)同工作,讓這種代碼執(zhí)行行為成為可能的。在這種特殊的情況下,我認(rèn)為呈現(xiàn)這些的最清晰方式是從字節(jié)碼開始逆向分析。我希望通過這個(gè)例子來介紹如何挖掘Python內(nèi)部[3]的信息(這是如此充滿樂趣?。?。

讓我們來看本文開篇提出的函數(shù)的一部分:

def foo(lst):
 a = 0
 for i in lst:
 a += i
 return a

產(chǎn)生的字節(jié)碼是:

 0 LOAD_CONST 1 (0)
 3 STORE_FAST 1 (a)
 
 6 SETUP_LOOP 24 (to 33)
 9 LOAD_FAST 0 (lst)
12 GET_ITER
13 FOR_ITER 16 (to 32)
16 STORE_FAST 2 (i)
 
19 LOAD_FAST 1 (a)
22 LOAD_FAST 2 (i)
25 INPLACE_ADD
26 STORE_FAST 1 (a)
29 JUMP_ABSOLUTE 13
32 POP_BLOCK
 
33 LOAD_FAST 1 (a)
36 RETURN_VALUE

作為提示,LOAD_FAST和STORE_FAST是字節(jié)碼(opcode),Python用它來訪問只在函數(shù)中使用的變量。由于Python編譯器知道(編譯時(shí))在每個(gè)函數(shù)中有多少個(gè)這樣的靜態(tài)變量,它們可以通過靜態(tài)數(shù)組偏移量而不是一個(gè)哈希表進(jìn)行訪問,這使得訪問速度更快(因而是_FAST后綴)。我有些離題了。這里真正重要的是變量a和i被平等對待。它們都通過LOAD_FAST獲取,并通過STORE_FAST修改。絕對沒有任何理由認(rèn)為它們的可見性是不同的。[4]

那么,這種執(zhí)行現(xiàn)象是怎么發(fā)生的?為什么編譯器認(rèn)為變量i只是foo中的一個(gè)局部變量。這個(gè)邏輯在符號表中的代碼中,當(dāng)編譯器執(zhí)行到AST開始創(chuàng)建一個(gè)控制流圖,隨后會(huì)產(chǎn)生字節(jié)碼。這個(gè)過程的更多細(xì)節(jié)在我有關(guān)符號表的文章中的介紹——所以我只在這里提及其中的重點(diǎn)。

符號表代碼并不認(rèn)為for語句很特別。在symtable_visit_stmt中有如下代碼:

case For_kind:
 VISIT(st, expr, s->v.For.target);
 VISIT(st, expr, s->v.For.iter);
 VISIT_SEQ(st, stmt, s->v.For.body);
 if (s->v.For.orelse)
 VISIT_SEQ(st, stmt, s->v.For.orelse);
 break;

索引變量如任何其他表達(dá)式一樣被訪問。由于該代碼訪問了AST,這值得去看看for語句結(jié)點(diǎn)內(nèi)部是怎樣的:

For(target=Name(id='i', ctx=Store()),
 iter=Name(id='lst', ctx=Load()),
 body=[AugAssign(target=Name(id='a', ctx=Store()),
 op=Add(),
 value=Name(id='i', ctx=Load()))],
 orelse=[])

所以i在一個(gè)名為Name的節(jié)點(diǎn)中。這些是由符號表代碼通過symtable_visit_expr中以下語句來處理的:

case Name_kind:
 if (!symtable_add_def(st, e->v.Name.id,
 e->v.Name.ctx == Load ? USE : DEF_LOCAL))
 VISIT_QUIT(st, 0);
 /* ... */

由于變量i被清楚地標(biāo)記為DEF_LOCAL(因?yàn)? _FAST字節(jié)碼是可訪問的,但是這也很容易觀察到,如果符號表是不能用的則使用symtable模塊),上述明顯的代碼調(diào)用symtable_add_def與DEF_LOCAL 作為第三個(gè)參數(shù)?,F(xiàn)在來瀏覽一下上面的AST,并注意到Name結(jié)點(diǎn)中i的ctx=Store部分。因此,它是在For結(jié)點(diǎn)的target部分存儲(chǔ)著i的信息的AST。讓我們看看這是如何實(shí)現(xiàn)的。

編譯器中的AST構(gòu)建部分越過了解析樹(這是源代碼中相當(dāng)?shù)讓拥谋硎尽恍┍尘百Y料可以在這里獲得),同時(shí)在其他事項(xiàng)中,在某些結(jié)點(diǎn)設(shè)置expr_context屬性,其中最顯著的是Name結(jié)點(diǎn)。想想看,這樣一來,在下面的語句:

foo = bar + 1

for和bar這兩個(gè)變量都將在Name結(jié)點(diǎn)中結(jié)束。但是bar只是被加載到這段代碼中,而for實(shí)際上被存儲(chǔ)到這段代碼中。expr_context屬性通過符號表代碼被用來區(qū)分當(dāng)前和未來使用[5] 。

回到我們for循環(huán)的索引變量。這些內(nèi)容將在函數(shù)ast_for_for_stmt——for語句創(chuàng)建AST——中處理。下面是該函數(shù)的相關(guān)部分:

static stmt_ty
ast_for_for_stmt(struct compiling *c, const node *n)
{
 asdl_seq *_target, *seq = NULL, *suite_seq;
 expr_ty expression;
 expr_ty target, first;
 
 /* ... */
 
 node_target = CHILD(n, 1);
 _target = ast_for_exprlist(c, node_target, Store);
 if (!_target)
 return NULL;
 /* Check the # of children rather than the length of _target, since
 for x, in ... has 1 element in _target, but still requires a Tuple. */
 first = (expr_ty)asdl_seq_GET(_target, 0);
 if (NCH(node_target) == 1)
 target = first;
 else
 target = Tuple(_target, Store, first->lineno, first->col_offset, c->c_arena);
 
 /* ... */
 
 return For(target, expression, suite_seq, seq, LINENO(n), n->n_col_offset,
 c->c_arena);
}

在調(diào)用函數(shù)ast_for_exprlist時(shí)創(chuàng)建了Store上下文,該函數(shù)為索引變量創(chuàng)建了一個(gè)結(jié)點(diǎn)(注意,for循環(huán)的索引變量還可能是一序列變量的元組,而不僅僅是一個(gè)變量)。

在介紹為什么for循環(huán)變量和循環(huán)中的其他變量一視同仁的過程中,這個(gè)函數(shù)是最后總要的一部分。在AST中進(jìn)行標(biāo)記之后,在符號表和虛擬機(jī)中用于處理循環(huán)變量的代碼與處理其他變量的代碼是相同的。
結(jié)束語

本文討論了Python中可能被認(rèn)為是“疑難雜癥”的某些特定行為。我希望這篇文章確實(shí)解釋了Python的變量和作用域的代碼執(zhí)行行為,說明了為什么這些行為是有用的而且永遠(yuǎn)不太可能改變,以及Python編譯器的內(nèi)部如何使其正常工作。感謝您的閱讀!

[1] 在這里,我很想開個(gè)Microsoft Visual C ++ 6的玩笑,但事實(shí)讓人有些不安,因?yàn)樵?015年這個(gè)博客的大部分讀者不會(huì)懂這個(gè)笑話(這反映了我的年齡,而不是我的讀者的能力)。

[2] 你可能會(huì)說,在執(zhí)行到break之前時(shí),dowithstuff(i)可以進(jìn)入if中。但是,這并不總是很方便。此外,根據(jù)Guido的解釋,這里對我們關(guān)注的問題做了一個(gè)很好的分離——循環(huán)被用于并只用于搜索。在搜索結(jié)束后,循環(huán)中的變量會(huì)發(fā)生什么已經(jīng)不是循環(huán)關(guān)注的事情。我覺得這是非常好的一點(diǎn)。

[3]: 通常我的文章中的代碼是基于Python 3。具體而言,我期待Python庫中將要完成的下一個(gè)版本(3.5)的default分支。但是對于這個(gè)特定的主題,在3.x系列中的任何版本的源代碼都應(yīng)該是可以工作的。

[4] 函數(shù)分解中另一件很明顯的事是,如果循環(huán)不執(zhí)行,為什么i仍然是不可見的,GET_ITER和FOR_ITER這對字節(jié)碼將我們的循環(huán)當(dāng)做一個(gè)迭代器,然后調(diào)用其__next__方法。如果這個(gè)調(diào)用最后以拋出StopIteration異常結(jié)束,虛擬機(jī)捕捉到這個(gè)異常然后結(jié)束循環(huán)。只有實(shí)際值被返回,虛擬機(jī)才會(huì)繼續(xù)對i執(zhí)行STORE_FAST,因此讓這個(gè)值存在,讓后續(xù)代碼可以引用。

[5] 這是一個(gè)奇怪的設(shè)計(jì),我懷疑這個(gè)設(shè)計(jì)的實(shí)質(zhì)是為了使用相對干凈的遞歸訪問AST中的代碼,如符號表代碼和CFG生成器。

聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com

文檔

講解Python中for循環(huán)下的索引變量的作用域

講解Python中for循環(huán)下的索引變量的作用域:我們從一個(gè)測試開始。下面這個(gè)函數(shù)的功能是什么? def foo(lst): a = 0 for i in lst: a += i b = 1 for t in lst: b *= i return a, b 如果你覺得它的功能是計(jì)算lst中所有元素的和與積,不要沮喪。通常很難發(fā)現(xiàn)這里的錯(cuò)誤。如果在大堆真實(shí)的代
推薦度:
標(biāo)簽: 詳解 python python的
  • 熱門焦點(diǎn)

最新推薦

猜你喜歡

熱門推薦

專題
Top