欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

 更新時(shí)間:2015年04月15日 10:43:32   作者:Eli Bendersky  
這篇文章主要介紹了講解Python中for循環(huán)下的索引變量的作用域,是Python學(xué)習(xí)當(dāng)中的基礎(chǔ)知識(shí),本文給出了Python3的示例幫助讀者理解,需要的朋友可以參考下

我們從一個(gè)測(cè)試開始。下面這個(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è)測(cè)試時(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)。

注意最后一句,讓我們?cè)囋嚕?br />  

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獲得變量和作用域的簡(jiǎn)單性,而不訴諸于hacks(例如在循環(huán)完成后,刪除定義在該循環(huán)中的所有變量——想想它可能引發(fā)的異常)或更復(fù)雜的作用域規(guī)則。

Python的作用域規(guī)則非常簡(jiǎn)單、優(yōu)雅:模塊、類以及函數(shù)的代碼塊可引入作用域。在函數(shù)體內(nèi),變量從它們定義到代碼塊結(jié)束(包括嵌套的代碼塊如嵌套函數(shù))都是可見的。當(dāng)然,對(duì)于局部變量、全局變量(以及其他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é)束后是可見及可訪問的,你對(duì)這樣的發(fā)現(xiàn)感到驚奇嗎?不,這正是Python的工作方式。那么,為什么索引變量的作用域被區(qū)別對(duì)待呢?

順便說一句,列表推導(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ù)目,可以使用這種簡(jiǎn)潔的方式。否則,你就必須有一個(gè)獨(dú)立的計(jì)數(shù)器。

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

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

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

多年來,許多用戶都想保留這種特性。但即使對(duì)于開發(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被平等對(duì)待。它們都通過LOAD_FAST獲取,并通過STORE_FAST修改。絕對(duì)沒有任何理由認(rèn)為它們的可見性是不同的。[4]

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

符號(hào)表代碼并不認(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)中。這些是由符號(hào)表代碼通過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é)碼是可訪問的,但是這也很容易觀察到,如果符號(hào)表是不能用的則使用symtable模塊),上述明顯的代碼調(diào)用symtable_add_def與DEF_LOCAL 作為第三個(gè)參數(shù)。現(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屬性通過符號(hào)表代碼被用來區(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)記之后,在符號(hà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的解釋,這里對(duì)我們關(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分支。但是對(duì)于這個(gè)特定的主題,在3.x系列中的任何版本的源代碼都應(yīng)該是可以工作的。

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

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

相關(guān)文章

最新評(píng)論