迭代器
迭代器是依附於迭代協議的對象——基本意味它有一個next方法(method),當調用時,返回序列中的下一個項目。當無項目可返回時,引發(raise)StopIteration異常。
迭代對象允許一次迴圈。它保留單次迭代的狀態(位置),或從另一個角度講,每次迴圈序列都需要一個迭代對象。這意味我們可以同時迭代同一個序列不只一次。將迭代邏輯和序列分離使我們有更多的迭代方式。
調用一個容器(container)的__iter__方法建立迭代對象是掌握迭代器最直接的方式。iter函數為我們節約一些按鍵。
>>> nums = [1,2,3] # note that ... varies: these are different objects>>> iter(nums) <listiterator object at ...>>>> nums.__iter__() <listiterator object at ...>>>> nums.__reversed__() <listreverseiterator object at ...>>>> it = iter(nums)>>> next(it) # next(obj) simply calls obj.next()1>>> it.next()2>>> next(it)3>>> next(it)Traceback (most recent call last): File "<stdin>", line 1, in <module>StopIteration
當在迴圈中使用時,StopIteration被接受並停止迴圈。但通過顯式引發(invocation),我們看到一旦迭代器元素被耗盡,存取它將引發異常。
使用for...in迴圈也使用__iter__方法。這允許我們透明地開始對一個序列迭代。但是如果我們已經有一個迭代器,我們想在for迴圈中能同樣地使用它們。為了實現這點,迭代器除了next還有一個方法__iter__來返回迭代器自身(self)。
Python中對迭代器的支援無處不在:標準庫中的所有序列和無序容器都支援。這個概念也被拓展到其它東西:例如file對象支援行的迭代。
>>> f = open('/etc/fstab')>>> f is f.__iter__()True
file自身就是迭代器,它的__iter__方法並不建立一個單獨的對象:僅僅單線程的順序讀取被允許。
產生運算式
第二種建立迭代對象的方式是通過 產生運算式(generator expression) ,列表推導(list comprehension)的基礎。為了增加清晰度,產生運算式總是封裝在括弧或運算式中。如果使用圓括弧,則建立了一個產生迭代器(generator iterator)。如果是方括弧,這一過程被‘短路'我們獲得一個列表list。
>>> (i for i in nums) <generator object <genexpr> at 0x...>>>> [i for i in nums][1, 2, 3]>>> list(i for i in nums)[1, 2, 3]
在Python 2.7和 3.x中列表運算式文法被擴充到 字典和集合運算式。一個集合set當產生運算式是被大括弧封裝時被建立。一個字典dict在運算式包含key:value形式的索引值對時被建立:
>>> {i for i in range(3)} set([0, 1, 2])>>> {i:i**2 for i in range(3)} {0: 0, 1: 1, 2: 4}
如果您不幸身陷古老的Python版本中,這個文法有點糟:
>>> set(i for i in 'abc')set(['a', 'c', 'b'])>>> dict((i, ord(i)) for i in 'abc'){'a': 97, 'c': 99, 'b': 98}
產生運算式相當簡單,不用多說。只有一個陷阱值得提及:在版本小於3的Python中索引變數(i)會泄漏。
產生器
產生器是產生一列結果而不是單一值的函數。
第三種建立迭代對象的方式是調用產生器函數。一個 產生器(generator) 是包含關鍵字yield的函數。值得注意,僅僅是這個關鍵字的出現完全改變了函數的本質:yield語句不必引發(invoke),甚至不必可接觸。但讓函數變成了產生器。當一個函數被調用時,其中的指令被執行。而當一個產生器被調用時,執行在其中第一條指令之前停止。產生器的調用建立依附於迭代協議的產生器對象。就像常規函數一樣,允許並發和遞迴調用。
當next被調用時,函數執行到第一個yield。每次遇到yield語句獲得一個作為next返回的值,在yield語句執行後,函數的執行又被停止。
>>> def f():... yield 1... yield 2>>> f() <generator object f at 0x...>>>> gen = f()>>> gen.next()1>>> gen.next()2>>> gen.next()Traceback (most recent call last): File "<stdin>", line 1, in <module>StopIteration
讓我們遍曆單個產生器函數調用的整個曆程。
>>> def f():... print("-- start --")... yield 3... print("-- middle --")... yield 4... print("-- finished --")>>> gen = f()>>> next(gen)-- start --3>>> next(gen)-- middle --4>>> next(gen) -- finished --Traceback (most recent call last): ...StopIteration
相比常規函數中執行f()立即讓print執行,gen不執行任何函數體中語句就被賦值。只有當gen.next()被next調用,直到第一個yield部分的語句才被執行。第二個語句列印-- middle --並在遇到第二個yield時停止執行。第三個next列印-- finished --並且到函數末尾,因為沒有yield,引發了異常。
當函數yield之後控制返回給調用者後發生了什嗎?每個產生器的狀態被儲存在產生器對象中。從這點看產生器函數,好像它是運行在單獨的線程,但這僅僅是假象:執行是嚴格單線程的,但解譯器保留和儲存在下一個值請求之間的狀態。
為何產生器有用?正如關於迭代器這部分強調的,產生器函數只是建立迭代對象的又一種方式。一切能被yield陳述式完成的東西也能被next方法完成。然而,使用函數讓解譯器魔力般地建立迭代器有優勢。一個函數可以比需要next和__iter__方法的類定義短很多。更重要的是,相比不得不對迭代對象在連續next調用之間傳遞的執行個體(instance)屬性來說,產生器的作者能更簡單的理解局限在局部變數中的語句。
還有問題是為何迭代器有用?當一個迭代器用來驅動迴圈,迴圈變得簡單。迭代器代碼初始化狀態,決定是否迴圈結束,並且找到下一個被提取到不同地方的值。這凸顯了迴圈體——最值得關注的部分。除此之外,可以在其它地方重用迭代器代碼。
雙向通訊
每個yield語句將一個值傳遞給調用者。這就是為何PEP 255引入產生器(在Python2.2中實現)。但是相反方向的通訊也很有用。一個明顯的方式是一些外部(extern)語句,或者全域變數或共用可變對象。通過將先前無聊的yield語句變成運算式,直接通訊因PEP 342成為現實(在2.5中實現)。當產生器在yield語句之後恢複執行時,調用者可以對產生器對象調用一個方法,或者傳遞一個值 給 產生器,然後通過yield語句返回,或者通過一個不同的方法向產生器注入異常。
第一個新方法是send(value),類似於next(),但是將value傳遞進作為yield運算式值的產生器中。事實上,g.next()和g.send(None)是等效的。
第二個新方法是throw(type, value=None, traceback=None),等效於在yield語句處
raise type, value, traceback
不像raise(從執行點立即引發異常),throw()首先恢複產生器,然後僅僅引發異常。選用單次throw就是因為它意味著把異常放到其它位置,並且在其它語言中與異常有關。
當產生器中的異常被引發時發生什嗎?它可以或者顯式引發,當執行某些語句時可以通過throw()方法注入到yield語句中。任一情況中,異常都以標準方式傳播:它可以被except和finally捕獲,或者造成產生器的中止並傳遞給調用者。
因完整性緣故,值得提及產生器迭代器也有close()方法,該方法被用來讓本可以提供更多值的產生器立即中止。它用產生器的__del__方法銷毀保留產生器狀態的對象。
讓我們定義一個只列印出通過send和throw方法所傳遞東西的產生器。
>>> import itertools>>> def g():... print '--start--'... for i in itertools.count():... print '--yielding %i--' % i... try:... ans = yield i... except GeneratorExit:... print '--closing--'... raise... except Exception as e:... print '--yield raised %r--' % e... else:... print '--yield returned %s--' % ans>>> it = g()>>> next(it)--start----yielding 0--0>>> it.send(11)--yield returned 11----yielding 1--1>>> it.throw(IndexError)--yield raised IndexError()----yielding 2--2>>> it.close()--closing--
注意: next還是__next__?
在Python 2.x中,接受下一個值的迭代器方法是next,它通過全域函數next顯式調用,意即它應該調用__next__。就像全域函數iter調用__iter__。這種不一致在Python 3.x中被修複,it.next變成了it.__next__。對於其它產生器方法——send和throw情況更加複雜,因為它們不被解譯器隱式調用。然而,有建議文法擴充讓continue帶一個將被傳遞給迴圈迭代器中send的參數。如果這個擴充被接受,可能gen.send會變成gen.__send__。最後一個產生器方法close顯然被不正確的命名了,因為它已經被隱式調用。
鏈式產生器
注意: 這是PEP 380的預覽(還未被實現,但已經被Python3.3接受)
比如說我們正寫一個產生器,我們想要yield一個第二個產生器——一個子產生器(subgenerator)——產生的數。如果僅考慮產生(yield)的值,通過迴圈可以不費力的完成:
subgen = some_other_generator()for v in subgen: yield v
然而,如果子產生器需要調用send()、throw()和close()和調用者適當互動的情況下,事情就複雜了。yield語句不得不通過類似於前一章節部分定義的try...except...finally結構來保證“調試”產生器函數。這種代碼在PEP 380中提供,現在足夠拿出將在Python 3.3中引入的新文法了:
yield from some_other_generator()
像上面的顯式迴圈調用一樣,重複從some_other_generator中產生值直到沒有值可以產生,但是仍然向子產生器轉寄send、throw和close。