迭代器
迭代器是依附于迭代協議的對象――基本意味它有一個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__()Truefile自身就是迭代器,它的__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。
新聞熱點
疑難解答
圖片精選