Python中yield的作用:迭代生成器

整理自公衆號:Devtogether

yield

爲了搞清楚 yield 和 return 這兩者的區別,我們先來看一個簡單的例子:

>>> def self_return(n):
...    print('rocky')
...    while n > 0:
...            print('before return')
...            return n
...            n -= 1
...            print('after return')
...
>>> s = self_return(3)
rocky
before return
>>> s
3

從上面的例子中函數 self_return(n) 被調用的過程中我們可以清晰的看出,s = self_return(3) 函數體內的語句就開始執行了,遇到 return 以後將值返回,並結束在函數體內的執行,所以我們看到的結果是 return 後面的語句根本沒有執行,這個是 return 的特點,不知道你還記得麼?如果不記得的話可以去翻我前面的文章。

下面我們來將 return 換乘 yield ,再來試試看:

>>> def self_yield(n):
...    print('rocky')
...    while n > 0:
...            print('before yield')
...            yield n
...            n -= 1
...            print('after yield')
...
>>> s = self_yield(3)
>>> s.__next__()
rocky
before yield
3

仔細觀察上面的例子你會發現,s = self_yield(n) 並沒有去執行函數體內的語句,且 s.next() 的時候遇到 yield 的時候,會返回值,並且暫停。我們接着再繼續來試一下:

>>> s.__next__()
after yield
before yield
2
>>> s.__next__()
after yield
before yield
1
>>> s.__next__()
after yield
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteration

通過上面的繼續操作,我們可以看到每次遇到 yield 的時候都會返回值,並且暫停,下次再執行的時候是從上次暫停的位置開始又繼續執行的,當沒有滿足條件的值,就會拋出異常。

結合上面的分析和對用例的執行結果,相信你已經你已經理解了 yield 的特點,也知道它與 return 之間的區別了:一般的函數,都是止於 return;作爲生成器的函數,因爲有了 yield,則遇到它會掛起。

下面我想再用一個例子來具體的闡述一下。斐波那契數列相信你們已經不陌生了,我在前面的文章中不止一次的提過它,這次我們嘗試將 yield 應用到斐波那契數列中:

def fibs(max):
   """
   fibonacci sequence generator
   """
   n, a, b = 0, 0, 1
   while n < max:
       yield b
       a, b = b, a + b
       n += 1

if __name__ == "__main__":
   f = fibs(9)
   for i in f:
       print(i,end = ' ')

上述代碼的運行結果如下:

1 1 2 3 5 8 13 21 34 55

你看,用生成器生成的斐波那契數列是不是跟以前的不一樣了呢?如果有興趣的話,你可以將我在前面文章中演示過的斐波那契數列的實現方式和現在的做一下對比,然後仔細觀察一下差異之處。

經過這幾次的各種演示,其實已經很明確了:在一個函數中如果有了 yield 語句,那麼它就是生成器,即也是迭代器。這種方式比前面寫迭代器的類要簡便的多,但這不是說迭代器不好,無論是使用迭代器還是生成器都要具體問題具體分析。

yield 的作用是在調用的時候返回相應的值,一次返回一個結果,在每個結果中間掛起函數的狀態(即暫停執行),下一次執行是從上次暫停的位置開始,繼續向下執行。

下面我們來做一道題,要求寫出「將一個全是整數的列表進行操作後只保留奇數」。相信大多數人都能很快的寫出下面這樣的函數:

def get_odd(lst):
   res = []
   for i in lst:
       if i % 2:
           res.append(i)
   return res

def main():
   lst = range(10)
   for i in get_odd(lst):
       print(i)

if __name__ == '__main__':
   main()

上面這個沒什麼難度,既然我們學了「生成器」,我在前面還這麼舔它,是不是我們該用生成器來做一下這道題?看看用生成器來做同樣的功能,到底有什麼不同:

def get_odd(lst):
   for i in lst:
       if i % 2:
           yield i

def main():
   lst = range(10)
   for i in get_odd(lst):
       print(i)

if __name__ == '__main__':
   main()

對比一下這個功能的兩種做法,使用「生成器」以後,代碼變的行數更少了(省去了對 res 的操作,不用把結果存在 res 裏),代碼整體看起來更清晰了(一看就知道幹嘛的,不用一上來去想 res 是個什麼鬼,append 進去的是個什麼玩意兒)。

2.生成器表達式

「生成器表達式」和列表推導式類似。區別在於使用列表推導,一次會產生所有的結果,而用「生成器表達式」則不會這樣,它是按需產生。

列表推導式的寫法如下:

>>> res = [x for x in range(5)]
>>> res
[0, 1, 2, 3, 4]

生成器表達式就是將上面的 [] 變成 () 即可:

>>> res = (x for x in range(5))
>>> res
<generator object <genexpr> at 0x109d9f570>
>>> next(res)
0
>>> next(res)
1
>>> next(res)
2
>>> next(res)
3

我們也順便簡單的看一下「生成器」的優勢在「生成器表達式」中是怎麼體現的。如果我們想對一系列整數求和,直接用生成器可以寫成下面這樣:

>>> sum((x for x in range(5)))
10

當然爲了方便起見,也可以省略圓括號,即寫成下面這樣:

>>> sum(x for x in range(5))
10

但是如果你用常規的寫法去寫,就會寫成下面這樣:

>>> sum([x for x in range(5)])
10

上面的代碼先構造了一個列表,然後再用 sum 函數求和,多了一步,天差地別,光在時間效率上,就已經輸掉了褲子。

所以綜合上面文章所講,「生成器」光在明面上的優點就有好幾個:代碼行數更少;代碼更易讀;時效更高...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章