技術|Python從零開始系列連載(二十)——生成器

還記得在迭代器裡我們說為什麼將列表轉為迭代器麼?( )

小明:因為列表太大的話佔用內存太大,做成迭代器可以節省空間,用的時候再拿出部分

是的,今天要講的生成器是不會把結果保存在一個系列中,而是保存生成器的狀態。

在每次進行迭代時返回一個值,直到遇到StopIteration異常結束。

見過這種東西吧:

技術|Python從零開始系列連載(二十)——生成器

你可以認為每一杯飲料就是一個生成的對象,我不會一次倒出所有的飲料

而是要喝的時候去倒出一杯(也就是需要的時候生成一個)

簡單方法創建生成器

我們看個例子:

技術|Python從零開始系列連載(二十)——生成器

我們發現,當要生成的list非常大時,拋出異常,存儲報錯。

那怎樣生成這種巨大的list呢?

技術|Python從零開始系列連載(二十)——生成器

你親手試一下,發現瞬間程序就運行結束了

我們看到,b是一個generator,也就是生成器模式

你應該已經注意到,生成器的創建很簡單,將列表生成式的中括號改成小括號即可

注意:這裡說的不是列表,因為列表的中括號改成小括號是元組!

那我們怎麼生成一個內容呢?

和之前的迭代器相同,使用next()函數即可:

技術|Python從零開始系列連載(二十)——生成器

直到最後會拋出異常,也就是到達了生成器的末端了

函數進化為生成器

還記得函數的定義麼?

我們在之前用遞歸定義了一個斐波那契數列

現在我們定義一個新的函數來生成斐波那契數列的第n項

技術|Python從零開始系列連載(二十)——生成器

為了實現後一項等於前兩項之和使用了a,b = b,a+b

為什麼這樣寫,留給大家思考~

提示:可以輸入n=3,自己感受一下調用函數過程中a和b的變化

值得注意的是,這個函數,當n=0時返回的是1,而不是正確的0

所以我們對其進行修改:

技術|Python從零開始系列連載(二十)——生成器

在循環之前,加了一個判斷

小明:老溼!你這個說的還是函數啊,和生成器有啥關係?說好的函數進化成生成器呢?

好的,我們看看函數怎麼進化為生成器!

技術|Python從零開始系列連載(二十)——生成器

我們把函數中的return換成yield

函數就進化成了生成器,當我們調用時,發現返回的是生成器對象

為了拿到數據,我們應該怎麼做呢?

小紅:老師,是不是可以試試next()函數呢?

對,不過在此之前,我們先要用一個變量去接收這個生成器對象

並且為了觀察生成器的特點,我們對函數進行修改!

仔細看好:

技術|Python從零開始系列連載(二十)——生成器

當我們使用next(a)對生成器操作一次時,會返回循環一次的值

也就是在yield處結束本次運行

但它的特點就是下次使用next(a)時,接著上次的斷點繼續運行,直到下一個yield

不斷使用next(a),直到運行到生成器結尾處,如下圖:

技術|Python從零開始系列連載(二十)——生成器

可能你對他的運行過程還不是特別清晰

我們加上print輸出來徹底搞懂他的運行過程:

技術|Python從零開始系列連載(二十)——生成器

發現每次返回值都是在yield的地方了吧~

小結:

  1. 講了兩種生成器創建方式
  2. 加了yield的函數就變成了生成器
  3. 要定義一個變量接收生成器的返回值
  4. 使用next()獲取生成器每次返回的值,並且斷點在yield處
  5. 下次使用next()從上次的斷電往下執行,直到生成器末端(這裡表現為循環結束)
  6. 生成器屬於迭代器,所以肯定是可迭代對象啦~

使用for循環調用生成器

我們使用next()去遍歷生成器的時候,我們不知道什麼時候會結束

而結束後再使用next()會拋出異常

因為生成器屬於迭代器

所以我們可以使用for循環去調用生成器

技術|Python從零開始系列連載(二十)——生成器

與next()等價的方式

技術|Python從零開始系列連載(二十)——生成器

send()強勢入場

我們先看一個例子:

技術|Python從零開始系列連載(二十)——生成器

發現每次運行,除了返回下一個,還會打印出None

觀察代碼,注意 item = yield i 這句

首先執行等號右邊的,yield返回,此時,返回生成器一個對象,並且中斷

在下次使用 f.__next__( )時候,並沒有傳內容進去,所以可以認為yield i 這整個賦值給item的為None

所以item打印出為None

為了做個比較,引入send()

技術|Python從零開始系列連載(二十)——生成器

send()可以看做next()的增強版

除了可以使用next()功能

還能傳入一個值到上次yield斷開地方的整體表達式(這裡傳給是yield i)

send()的坑:

技術|Python從零開始系列連載(二十)——生成器

這是為什麼呢?

6

send()退化為next()

小紅:老師,我想了兩天終於想到了!

哦?你說說

小紅:因為 f.send()取生成器第一個對象,並且傳入了一個參數‘Python’

但是,並沒有變量去接收這個參數!所以報錯了!

是的,之前我們的代碼是這樣的:

技術|Python從零開始系列連載(二十)——生成器

注意,我們調用第一個對象使用next,並沒有傳入參數!

而第二次使用send調用下一個對象時候,傳入的參數相當於代替了yield i

也就是賦值給了item,所以沒報錯

小明:老溼,你說send是加強版的next,我想給send退化到next,可以麼?

這個嘛,是可以的!小明你試試!

小明:大家都退後!我要裝逼了!

技術|Python從零開始系列連載(二十)——生成器

技術|Python從零開始系列連載(二十)——生成器

小明:不傳參數竟然不行!!!

技術|Python從零開始系列連載(二十)——生成器

小明啊,應該是這樣的!

技術|Python從零開始系列連載(二十)——生成器

傳入None才對!

小明:我不服!老溼,你說這生成器很厲害,具體有啥應用呢?學了沒用豈不是很雞肋?

這個嘛!其實是有用的,我先劇透一下!

技術|Python從零開始系列連載(二十)——生成器

多任務——協程

我們先介紹一下多任務

多任務處理是指用戶可以在同一時間內運行多個應用程序,每個應用程序被稱作一個任務

簡單點說,就是

你現在可能邊看這篇文章邊聽著音樂

技術|Python從零開始系列連載(二十)——生成器

而看文章是一個任務(這裡的任務指正在做的事情)

聽音樂也是一個任務

你同時在做這兩件事

就是多任務啦

電腦和人還是不一樣的

我們今天講一下協程來完成多任務(之後還會講到線程、進程來完成多任務)

考慮一個工廠流水線

A機器每次將一件貨物放入箱子

B機器每次將A機器的箱子封箱打包

為了不產生問題,必須A完成一個,B接著完成一個

並且看起來兩個任務是同時執行的!

技術|Python從零開始系列連載(二十)——生成器

在Python中可以用生成器實現簡單的協程:

技術|Python從零開始系列連載(二十)——生成器

我們來看這個程序,先定義了兩個生成器(不是函數哦~原因在之前課程講過)

當我們在while主程序中,先使用f1.__next__( )調用生成器func1,因為fun1的循環條件始終為真

所以先打印(執行裝入操作)然後遇到 yield 退出生成器func1,回到主程序

接著執行f2.__next__( )調用生成器func2,像之前調用func1一樣,先打印(執行打包操作)

然後遇到yield退出生成器func2,回到主程序

因為主程序循環條件始終為真,所以繼續像之前一樣,接著使用f1.__next__( )調用生成器func1。。。

如此往復。。。

我是使用打斷來停止程序執行的,不然會不斷執行下去

由於兩個生成器(任務)交替執行,很快

就像在多任務執行

所以,通俗理解看上去同時執行的就是多任務~

小明:竟然有這種操作!

今天作業:

敲一遍代碼,理解一下兩種生成器創建方式

自己不敲代碼永遠學不會寫代碼

下課

人生苦短,我選Python

未完待續,連載中......


分享到:


相關文章: