Python--迭代器,生成器,yield函數的作用

主要分兩部分介紹:

  1. 迭代、迭代器、可迭代
  2. 生成器、yield 表達式

1. 迭代、迭代器、可迭代
(1)迭代概念:很多數據就是容器,裏面包含很多其他類型的元素。實際使用容器時,我們需要逐個獲取容器中的元素。逐個獲取容器中的元素的過程就叫做迭代。

迭代demo:
list_a = [1,2,3]
for i in list_a:
      print(i)

上述逐個取list_a對象中元素並打印的過程就叫做迭代,其中迭代的對象是list_a,可以看出其爲列表,因此列表是可迭代的;Python中其他的數據類型:dict,tuple,string,set,file等都是可迭代的。對於用戶自己自定義的數據類型,如果提供了__iter__()或者__getitem__()方法,那麼該類方法實例化後的對象也是可迭代的;
(2)迭代器----是一種對象。
迭代器抽象的是一個數據流,是只允許迭代一次的對象。對迭代器不斷調用A().next()方法(Python2.7)或者next(A())(python3),則可以依次獲得下一個元素;當迭代器中沒有元素時,調用next()方法會拋出StopIteration異常。迭代器的 iter() 方法返回迭代器自身;因此迭代器也是可迭代的。
(3)迭代器協議 ----是指容器類需要包含的一個特殊方法 iter()
如果一個容器類提供了 iter() 方法,並且該方法能返回一個能夠逐個訪問容器內所有元素的迭代器,則我們說該容器類實現了迭代器協議。
Python 中的迭代器協議和 Python 中的 for 循環是緊密相連的。

# iterator protocol and for loop
for x in something:
    print(x)

Python 處理 for 循環時,首先會調用內建函數 iter(something),它實際上會調用 something.iter(),返回 something 對應的迭代器。而後,for 循環會調用內建函數 next(),作用在迭代器上,獲取迭代器的下一個元素,並賦值給 x。此後,Python 纔開始執行循環體。

2. 生成器、yield表達式
(1)生成器函數----是一種特殊的函數,是指包含了yield表達式的函數,調用它會返回特殊的迭代器,稱爲生成器;

def func():
    return 1

def gen():
    yield 1

print(type(func))   # <class 'function'>
print(type(gen))    # <class 'function'>

print(type(func())) # <class 'int'>
print(type(gen()))  # <class 'generator'>

如上,生成器 gen 看起來和普通的函數沒有太大區別。僅只是將 return 換成了 yield。用 type() 函數打印二者的類型也能發現,func 和 gen 都是函數。然而,二者的返回值的類型就不同了。func() 是一個 int 類型的對象;而 gen() 則是一個迭代器對象。
(2)yield表達式
(2.1) yield 僅能用於定義生成器函數;
(2.2)生成器函數被調用後,其函數體內的代碼並不會立即執行,而是返回一個生成器(generator-iterator)。當返回的生成器調用成員方法時,相應的生成器函數中的代碼纔會執行;
詳情: 如前所述,如果一個函數定義中包含 yield 表達式,那麼該函數是一個生成器函數(而非普通函數)。實際上,yield 僅能用於定義生成器函數。
與普通函數不同,生成器函數被調用後,其函數體內的代碼並不會立即執行,而是返回一個生成器(generator-iterator)。當返回的生成器調用成員方法時,相應的生成器函數中的代碼纔會執行。

def square():
    for x in range(4):
        yield x ** 2
square_gen = square()
for x in square_gen:
    print(x)

前面說到,**for 循環會調用 iter() 函數,獲取一個生成器;而後調用 next() 函數,將生成器中的下一個值賦值給 x;再執行循環體。**因此,上述 for 循環基本等價於:

genitor = square_gen.__iter__()
while True:
    x = geniter.next() # Python 3 是 __next__()
    print(x)

注意到,square 是一個生成器函數;作爲它的返回值,square_gen 已經是一個迭代器;迭代器的 iter() 返回它自己。因此 geniter 對應的生成器函數,即是 square。

每次執行到 x = geniter.next() 時,square 函數會從上一次暫停的位置開始,一直執行到下一個 yield 表達式,將 yield 關鍵字後的表達式列表返回給調用者,並再次暫停。注意,每次從暫停恢復時,生成器函數的內部變量、指令指針、內部求值棧等內容和暫停時完全一致。

(2.3)yield函數的好處:
Python 的老用戶應該會熟悉 Python 2 中的一個特性:內建函數 range 和 xrange。其中,range 函數返回的是一個列表,而 xrange 返回的是一個迭代器。

在 Python 3 中,range 相當於 Python 2 中的 xrange;而 Python 2 中的 range 可以用 list(range()) 來實現。

Python 之所以要提供這樣的解決方案,是因爲在很多時候,我們只是需要逐個順序訪問容器內的元素。大多數時候,我們不需要「一口氣獲取容器內所有的元素」。比方說,順序訪問容器內的前 5 個元素,可以有兩種做法::
方法1:獲取容器內的所有元素,然後取出前 5 個;
方法2:從頭開始,逐個迭代容器內的元素,迭代 5 個元素之後停止。

顯而易見,如果容器內的元素數量非常多(比如有 10 ** 8 個),或者容器內的元素體積非常大,那麼後一種方案能節省巨大的時間、空間開銷。

現在假設,我們有一個函數,其產出(返回值)是一個列表。而若我們知道,調用者對該函數的返回值,只有逐個迭代這一種方式。那麼,如果函數生產列表中的每一個元素都需要耗費非常多的時間,或者生成所有元素需要等待很長時間,則使用 yield 把函數變成一個生成器函數,每次只產生一個元素,就能節省很多開銷了。

#Python3
def f1():
    a =[1,2]
    yield a
f2 = f1()
print(f1())
print(dir(f1()))
print(next(f2))
# print(next(f2)) #取消註釋,讓其運行會報StopIteration錯誤;
結果爲:
<generator object f1 at 0x7fbf63840468>
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']
[1, 2]
可以看出,yield可迭代對象本身是會還原可迭代列表本身的;但是yield本身的優勢及更主要的作用是:當列表較大的時候,如果我們只需去列表的前5個用於運算的時候,不需要等待上層程序將結果列表全部計算出來,再選擇前5個,而是可以直接用yield的函數,運行一次,計算一次;提高運行效率;
f1().next()
AttributeError: 'generator' object has no attribute 'next'
在python2.7中,實例化對象用next:f1().next();
在Python3中,實例化對象用next:next(f1())

3.generator生成器方法

生成器有一些方法。調用這些方法可以控制對應的生成器函數;不過,若是生成器函數已在執行過程中,調用這些方法則會拋出 ValueError 異常。

  1. generator.next():從上一次在 yield 表達式暫停的狀態恢復,繼續執行到下一次遇見 yield 表達式。當該方法被調用時,當前 yield 表達式的值爲 None,下一個 yield 表達式中的表達式列表會被返回給該方法的調用者。若沒有遇到 yield 表達式,生成器函數就已經退出,那麼該方法會拋出 StopIterator 異常。
  2. generator.send(value):和 generator.next() 類似,差別僅在與它會將當前 yield 表達式的值設置爲 value。
  3. generator.throw(type[, value[, traceback]]):向生成器函數拋出一個類型爲 type 值爲 value 調用棧爲 traceback 的異常,而後讓生成器函數繼續執行到下一個 yield 表達式。其餘行爲與 generator.next() 類似。
  4. generator.close():告訴生成器函數,當前生成器作廢不再使用。
    參考:https://liam.page/2017/06/30/understanding-yield-in-python/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章