Python中的迭代器與生成器進階用法

高洛峰
發布: 2017-03-01 14:09:14
原創
1074 人瀏覽過

迭代器

迭代器是依附於迭代協定的物件-基本意義它有一個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(&#39;/etc/fstab&#39;)
>>> 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 &#39;abc&#39;)
set([&#39;a&#39;, &#39;c&#39;, &#39;b&#39;])
>>> dict((i, ord(i)) for i in &#39;abc&#39;)
{&#39;a&#39;: 97, &#39;c&#39;: 99, &#39;b&#39;: 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 &#39;--start--&#39;
...   for i in itertools.count():
...     print &#39;--yielding %i--&#39; % i
...     try:
...       ans = yield i
...     except GeneratorExit:
...       print &#39;--closing--&#39;
...       raise
...     except Exception as e:
...       print &#39;--yield raised %r--&#39; % e
...     else:
...       print &#39;--yield returned %s--&#39; % 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。

更多Python中的迭代器与生成器高级用法相关文章请关注PHP中文网!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!