迭代器:初探
上一章曾經提到過,其實for迴圈是可用於任何可迭代的對象上的。實際上,對Python中所有會從左至右掃描對象的迭代工具而言都是如此,這些迭代工具包括了for迴圈、列表解析、in成員關係測試以及map內建函數等。
“可迭代對象”的概念在Python中是相當新穎的,基本這就是序列觀念的通用化:如果對象時實際儲存的序列,或者可以再迭代工具環境中一次產生一個結果的對象,那就看做是可迭代的。
>>檔案迭代器
作為內建資料類型的檔案也是可迭代的,它有一個名為__next__的方法,每次調用時,就會返迴文件中的下一行。當到達檔案末尾時,__next__會引發內建的StopIteration異常,而不是返回Null 字元串。
這個介面就是Python中所謂的迭代協議:有__next__方法的對象會前進到下一個結果,而在一系列結果的末尾時,則會引發StopIteration。任何這類對象都認為是可迭代的。任何這類對象也能以for迴圈或其他迭代工具遍曆,因為所有迭代工具內部工作起來都是在每次迭代中調用__next__,並且捕捉StopIteratin異常來確定何時離開。
for line in open('script.py'): print(line.upper(),end='')
上面的代碼就是檔案迭代的一個例子,並且這種用法是最高效的檔案讀取方法,主要有三個優點:這是最簡單的寫法,運行快,並且從記憶體使用量情況來說也是最好的。
替代的寫法是:
for line in open('script.py').readlines(): print(line.upper(),end='')
這種調用方法會把檔案一次性讀到記憶體中,如果檔案太大,那麼記憶體會被消耗光的。
>>手動迭代:iter和next
為了支援手動迭代代碼(用較少的錄入),Python3.0還提供了一個內建函數next,它會自動調用一個對象的__next__方法。給定一個對象X,調用next(X)等同於X.__next__(),但前者簡單很多。
從技術角度來講,迭代協議還有一點值得注意。當for迴圈開始時,會通過它傳給iter內建函數,以便從可迭代對象中獲得一個迭代器,返回的對象含有需要的next方法。調用iter的步驟對於檔案來說不是必須的,因為檔案對象就是自己的迭代器,但是對於其他的一些內建資料類型來說,就不一定了。
列表以及很多其他的內建對象,不是自身的迭代器,因為它們支援多次開啟迭代器。對這樣的對象,我們必須調用iter來啟動迭代:
L=[1,2,3]iter(L) is L #return falseL.__next__() #會報錯I = iter(L)I.__next__()I.__next__()
雖然Python迭代工具自動調用這些(iter,__next__)函數,我們也可以使用它們來手動地應用迭代協議。
列表解析:初探
>>列表解析基礎知識
L=[1,2,3,4,5]L = [x+10 for x in L]
列表解析寫在一個方括弧中,因為它們最終是構建一個新的列表的一種方式。它們以我們所組成的一個任意的運算式開始,該運算式使用我們所組成的一個迴圈變數(x+10)。這後面跟著我們現在應該看作是一個for迴圈頭部的部分,它申明了迴圈變數,以及一個可迭代對象(for x in L)
要運行該運算式,Python在解譯器內部執行一個遍曆L的迭代,按照順序把x賦給每個元素,並且收集對各元素運行左邊的運算式的結果。我們得到的結果清單就是列表解析所表達的內容——針對L中的每個x,包含了x+10的一個新列表。
其實列表解析式並不是必須的,因為它能完成的工作都能夠通過for迴圈完成,但是列表解析式比手動的for迴圈語句運行得更快(往往速度快一倍),因為它們的迭代在解譯器內部是以C語言的速度執行的,而不是以手動的Python代碼執行的,特別是對於較大的資料集合,這是使用列表解析的一個主要的效能優點。
當我們考慮在一個序列中的每個項上執行一個操作時,都可以考慮使用列表解析。
>>擴充的列表解析文法
實際上,列表解析可以有更進階的應用。作為一個特別有用的擴充,運算式中嵌套的for迴圈可以有一個相關的if子句,來過濾那些測試不為真的結果項。
lines = [line.rstrip() for line in open('script.py') if line[0]='p']
這條if子句檢查從檔案讀取的每一行,看它的第一個字元是否是p;如果不是,從結果清單中省略改行。
事實上,如果我們願意的話,列表解析可以變得更加複雜——它們的完整文法允許任意數目的for子句,每個子句有一個可選的相關的if子句。
Python3.0中的新的可迭代對象
Pyton3.0中的一個基本的改變是,它比Python2.x更強調迭代。除了與檔案和字典這樣的內建類型相關的迭代,字典方法keys、values和items都在Python3.0中返回可迭代對象。 返回一個可迭代對象而不是返回一個結果清單的好處在於節省了記憶體的空間。
>>多個迭代器VS單個迭代器
多個迭代器:在它們的結果中能保持不同位置的多個迭代器
單個迭代器:只能保持一個迭代器,在遍曆其結果之後,它們就用盡了。
通常通過針對iter調用返回一個新的對象,來支援多個迭代器;單個迭代器一般意味著一個對象返回其自身。
>>字典視圖迭代器
在Python3.0中,字典的keys、values和items方法返回可迭代的視圖對象,它們一次產生一個結果項,而不是在記憶體中一次產生全部結果清單。視圖項保持和字典中的那些項相同的物理順序,並且反映對底層的字典做出的修改。
和所有迭代器一樣,我們總可以通過把一個Python3.0字典視圖傳遞到list內建函數中,從而強制構建一個真正的列表。然而,這通常不是必須的。
此外,Python3.0字典仍然有自己的迭代器,它返回連續的鍵。因此,無需直接在此環境中調用keys:
for key in D:print(key,end='')
>>列表解析與map
列表解析在一個序列上的值應用一個任意運算式,將其結果搜集到一個新的列表中並返回。從文法上說,列表解析是由方括弧封裝起來的(為了提醒你它們構造了一個列表)。它們的簡單形式是在方括弧中編寫一個運算式,Python之後將這個運算式的應用迴圈中每次迭代的結果搜集起來。例如,假如我們希望搜集字串中的所有字元的ASCII碼,可以這樣做:
#迴圈的方法res=[]for x in 'spam': res.append(ord(x))#map函數的方法res=list(map(ord,'spam'))#列表解析res=[ord(x) for x in 'spam']
>>增加測試和嵌套迴圈
其實,列表解析還要比上面說的通用的多,我們可以在for之後編寫一個if分支,用來增加選擇邏輯。
#列表解析[x ** 2 for x in range(10) if x % 2 == 0]#maplist(map((lambda x:x**2),filter((lambda x: x % 2==0),range(10))))
上述的兩行代碼都是搜集了0~9中的偶數的平方和,可以很明顯的看到,完成同樣的功能,列表解析的語句簡單地多。
實際上,列表解析還能夠更加通用。你可以在一個列表解析中編寫任意數量的嵌套的for迴圈,並且每一個都有可選的關聯的if測試。通用結構如下所示:
expression for target1 in iterable1 [if comdition1] for target2 in iterable2 [if condition2] ... for targetN in iterableN [if conditionN]
當for分句嵌套在列表解析中時,它們工作起來就像等效的嵌套的for迴圈語句。例如,如下的代碼:
res=[x+y for x in [0,1,2] for y in [100,200,300]]
與下面如此冗長的代碼有相同的效果:
res=[]for x in [0,1,2]: for y in [100,200,30]: res.append(x+y)
>>列表解析和矩陣
使用Python編寫矩陣的一個基本的方法就是使用嵌套的列表結構,例如,下面的代碼定義了兩個3x3的矩陣:
M=[[1,2,3], [4,5,6], [7,8,9]]N=[[2,2,2], [3,3,3], [4,4,4]]
列表解析也是處理這樣結構的強大工具,它們能夠自動掃描行和列。
取出第二列的所有元素:
[row[1] for row in M] #[2,5,8][M[row][1] for row in (0,1,2)] #[2,5,8]
取出對角線上的元素:
[M[i][i] for i in range(len(M))] #[1,5,9]
混合多個矩陣,下面的代碼建立了一個單層的列表,其中包含了矩陣對元素的乘積。
複製代碼 代碼如下:
[M[row][col] * N[row][col] for row in range(3) for col in range(3)] #[2,4,6,12,15,18,28,32,36]
下面的代碼再複雜一點,構造一個嵌套的列表,其中的值與上面的一樣:
複製代碼 代碼如下:
[[M[row][col] * N[row][col] for col in range(3)] for row in range(3)] #[[2,4,6],[12,15,18],[28,32,36]]
上面的最後一個比較難於理解,它等同於如下基於語句的代碼:
res=[]for row in range(3): tmp=[] for col in range(3): tmp.append(M[row][col]*N[row][col]) res.append(tmp)
>>理解列表解析
基於對運行在當前Python版本下的測試,map調用比等效的for迴圈要快兩倍,而列表解析往往比map調用要稍快一些。速度上的差距來自底層實現,map和列表解析是在解譯器中以C語言的速度來啟動並執行,比Python的for迴圈在PVM中步進要快得多。
重訪迭代器:產生器
如今的Python對延遲提供更多的支援——它提供了工具在需要的時候才產生結果,而不是立即產生結果。特別地,有兩種語言結構儘可能地拖延結果建立。
產生器函數:編寫為常規的def語句,但是是使用yield語句一次返回一個結果,在每個結果之間掛起和繼續它們的狀態。
產生器運算式:產生器運算式類似於上一小節的列表解析,但是,它們返回按需產生結果的一個對象,而不是構建一個結果清單。
由於上面二者都不會一次性構建一個列表,它們節省了記憶體空間,並且允許計算時間分散到各個結果請求。
>>產生器函數:yield VS return
之前我們寫的函數都是接受輸入參數並立即返回單個結果的常規函數,然而,也有肯能來編寫可以送回一個值並隨後從其退出的地方繼續的函數。這樣的函數叫做產生器函數,因為它們隨著時間產生值的一個序列。
一般來說,產生器函數和常規函數一樣,並且,實際上也是用常規的def語句編寫的。然後,當建立時,它們自動實現迭代協議,以便可以出現在迭代背景中。
狀態掛起
和返回一個值並退出的函數不同,產生器函數自動在產生值的時刻掛起並繼續函數的執行。因此,它們對於提前計算整個一系列值以及在類中手動儲存和恢複狀態都很有用。由於產生器函數在掛起時儲存的狀態包含了它們的整個本地範圍,當函數恢複時,它們的本地變數保持了資訊並且使其可用。
產生器函和常規函數之間的主要不同之處在於,產生器yield一個值,而不是return一個值。yield語句掛起該函數並向調用者發送一個值,但是,保留足夠的狀態以使得函數從它離開的地方繼續。當繼續時,函數在上一個yield返回立即繼續執行。從函數角度來看,這允許其代碼隨著時間產生一系列的值,而不是一次計算它們並在諸如列表的內容中送回它們。
迭代協議整合
可迭代對象定義了一個__next__方法,它要麼返回迭代中的下一項,要麼引發一個特殊的StopIteration異常來終止迭代。一個對象的迭代器用iter內建函數接受。
如果支援該協議的話,Python的for迴圈以及其他的迭代技術,使用這種迭代協議來便利一個序列或值產生器;如果不支援,跌打返回去重複索引序列。
要支援這一協議,函數包含一條yield語句,該語句特別編譯為產生器。當調用時,它們返回一個迭代器對象,該對象支援用一個名為__next__的自動建立的方法來繼續執行的介面。產生器函數也可能有一條return語句,總是在def語句塊的末尾,直接終止值的產生。
產生器函數應用
def gensquares(N): for i in range(N): yield i ** 2
這個函數在每次迴圈時都會產生一個值,之後將其返回給它的調用者。當它被暫訂後,它的上一個狀態儲存了下來 並且在yield語句之後把控制器馬上回收。它允許函數避免臨時再做所有的工作,當結果的列表很大或者在處理每一個結果都需要很多時間時,這一點尤其重要。產生器將在loop迭代中處理一系列值的時間分布開來。
擴充產生器函數協議:send和next 在Python2.5中,產生器函數協議中增加了一個send方法。send方法產生一系列結果的下一個元素,這一點像__next__方法一樣,但是它提供了一種調用者與產生器之間進行通訊的方法,從而能夠影響它的操作。
def gen(): for i in range(10): X =yield i print(X)G = gen()next(G) #0G.send(77) #77 1G.send(88) #88 2next(G) #None 3
上面的代碼比較難於理解,而且書上的翻譯比較劣質,沒看懂。在網上查了一些資料,結合自己的理解,上述代碼的運行過程應該是這樣的:產生了一個函數對象,賦值給了G,然後調用了next() 函數,產生了產生器的第一個值0,所以傳回值是0。此時函數運行到yield語句,碰到yield語句後立即掛起函數,儲存狀態,等待下一次迭代。程式中之後又調用了send()方法,將77傳遞給了yield語句,yield語句將send()傳遞過來的值(此處是77)賦值給X,然後列印出來。然後函數繼續運行,直到再次碰到yield,此時是第二次碰到yield,所以返回了1,接著函數又被掛起,等待下一次迭代。接著又調用了send(),同上次調用一樣,將傳進的參數(此處是88)作為yield的傳回值賦值給X,然後列印,接著繼續運行函數,直到再次碰到yield,此時是第三次,因此輸出2。最後又再次調用了next()函數,其實'next()'函數就是傳遞了一個None,因此,我們得到的結果是None和3。
此處需要注意的是,其實next()和send(None)是等價的。通過send()方法,我們就能夠和產生器實現通訊。
>>產生器運算式:迭代器遇到列表解析
在最新版的Python中,迭代器和列表解析的概念形成了這種語言的一個新特性——產生器運算式。從文法上講,產生器運算式就像一般的列表解析一樣,但是它們是擴在圓括弧中而不是方括弧中的。
[x ** 2 for x in range(4)] #List comprehension:build a list(x ** 2 for x in range(4)) #Genterator expression:make an iterable
從執行過程上來講,產生器運算式很不相同:不是在記憶體中構建結果,而是返回一個產生器對象,這個對象將會支援迭代協議。
產生器運算式大體上可以認為是對記憶體空間的最佳化,他們不需要像方括弧的列表解析一樣,一次構造出整個結果清單。它們在實際中運行起來可能稍慢一些,所以它們可能只對於非常龐大的結果集合的運算來說是最優的選擇。
>>產生器函數VS產生器運算式
產生器函數和產生器運算式自身都是迭代器,並由此只支援一次活躍迭代,我們無法有在結果集中位於不同位置的多個迭代器。
Python3.0解析文法概括
我們已經在本章中關注過列表解析和產生器,但是,別忘了,還有兩種在Python3.0中可用的解析運算式形式:集合解析和字典解析。
[x*x for x in range(10)] #List comprehension:build list(x*x for x in range(10)) #Generator expression:produces items{x*x for x in range(10)} #Set comprehension:new in 3.0{x:x*x for x in range(10) #Directionary comprehension:new in 3.0
需要注意的是,上面最後兩種表達方式都是一次性構建所有對象的,它們沒有根據需要產生結果的概念。
總結
列表解析、集合解析、字典解析都是一次性構建對象的,直接返回的。
產生器函數和產生器運算式不是在記憶體中構建結果的,它們是返回一個產生器對象,這個對象支援迭代協議,根據調用者需要產生結果。
集合解析、字典解析都支援嵌套相關的if子句從結果中過濾元素。
函數陷阱
>>本地變數是靜態檢測的
Python定義的在一個函數中進行分配的變數名時預設為本地變數的,它們存在於函數的範圍並只在函數運行時存在。Python靜態檢測Python的本地變數,當編譯def代碼時,不是通過發現指派陳述式在運行時進行檢測的。被賦值的變數名在函數內部是當做本地變數來對待的,而不是僅僅在賦值以後的語句才被當做是本地變數。
>>沒有return語句的函數
在Python函數中,return(以及yield)語句是可選的。從技術上說,所有的函數都會返回了一個值,如果沒有提供return語句,函數將自動返回None對象。