一、列表生成式
顧名思義,列表生成式就是用於生成列表的特殊語法形式的表達式。
1.1 語法格式
[exp for iter_var in iterable]
工作過程:
1.通過iter_var迭代iterable中的每個元素
2.結合迭代的元素iter_var和exp表達式計算出結果
3.以列表形式返回每次迭代后exp表達式的計算值
由此可見我們最終得到的是一個列表,因此整個表達式是放在列表符號[]中的。
以上語法格式僅僅是最簡單的列表生成式形式,實際應用中還可以增加條件判斷(過濾)和嵌套循環等稍微復雜一些的處理邏輯,相應增加的邏輯處理就是在 每次迭代時先對迭代的對象進行條件判斷和嵌套循環處理,符合條件或處理完嵌套循環邏輯后再通過exp表達式來獲得當前迭代過程中的計算值。對應的形式如下:
- 帶條件判斷的列表生成式
[exp for iter_var in iterable if_exp]
- 帶嵌套循環的列表生成式
[exp for iter_var_A in iterable_A for iter_var_B in iterable_B]
1.2 應用場景
通過上述對列表生成式的語法形式不難看出,列表生成式是python提供的一種快速生成一個新的列表的簡潔方式(一條語句中可以包含條件判斷和嵌套循環)。它最主要的應用場景是:根據已存在的可迭代對象推導出一個新的list(可迭代對象即可應用於for循環的對象,下文會有更詳解的介紹)。
1.3 應用舉例
下面結合實例來對比下使用列表生成式和不使用列表生成式的情況:
- 生成一個簡單列表
生成一個從1到10的整數組成的列表:
(1)不使用列表生成式
1 list1 = [] 2 for i in range(1,11): 3 list1.append(i)
(2)使用列表生成式
1 list1 = [i for i in range(1, 11)]
- 生成一個帶條件判斷的列表
生成一個從1到10之間由偶數組成的列表:
(1)不使用列表生成式
1 list1 = [] 2 for i in range(1,11): 3 if i % 2 == 0: 4 list1.append(i)
(2)使用列表生成式
1 list1 = [i for i in range(1, 11) if i % 2 == 0]
- 生成一個帶嵌套循環的列表
計算兩個的全排列,並將結果以元組形式保存到一個新的列表中
(1)不使用列表生成式
1 key_list = ['Python', 'PHP', 'JAVA'] 2 value_list = ['coding', 'learning'] 3 new_list = [] 4 for i in key_list: 5 for j in value_list: 6 new_list.append((i,j)) 7 print(new_list)
(2)使用列表生成式
1 key_list = ['Python', 'PHP', 'JAVA'] 2 value_list = ['coding', 'learning'] 3 new_list = [] 4 new_list = [(i, j) for i in key_list for j in value_list] 5 print(new_list)
上述多個示例充分說明,使用列表生成式明顯要更方便簡潔。
二、生成器
2.1 生成器的誕生背景
經過上文對列表生成式的講解,我們發現列表生成式似乎很好很強大,但任何事物都有兩面性,仔細分析列表生成式的過程和本質就可以看出列表生成式是直接生成一個新的列表,然后把所有的元素都一次性地存放在內存中,因此存在以下缺陷:
- 內存容量總是有限的,因此列表容量有限(這點還能接受);
- 當列表中的元素很多時,勢必會占用大量的內存,而如果我們恰恰僅僅需要訪問其中的部分元素甚至說前面幾個元素時,就會造成內存的極度浪費;
- 與第二點相對應的是,此時系統反應很慢,生成需要的列表耗時很長。
因此當元素的數量達到一定的級別時,使用列表生成式就不太明智了。怎么解決這些問題呢?
如果列表元素可以按照某種既定的算法推算出來,那我們是否可以在循環的過程中不斷推算出后續的元素呢?這樣就不必創建完整的list,從而節省大量的空間。在Python中,這種一邊循環一邊計算的機制,稱為生成器:generator。(這段文字源自傳送門)
2.2 生成器的本質理解
從上面的論述可以提煉出生成器的以下本質過程:
生成器按照指定的算法,結合循環不斷推算出后續的元素(每循環一次就推算一個),因此並不是簡單地一上來就一股腦地生成出所有的數據,而是在調用(循環)時才生成。列表的長度在動態變化,可長可短(取決於循環的次數),因此非常有利於控制對內存的占用,可節省大量內存空間(需要使用多少就申請分配多少)。
這也使得通過生成器生成的元素幾乎是沒有限制的,相應的操作返回時間也很理想。
需要注意生成器的一個特性是,在循環推算過程中,它只能記錄的當前的位置,並往后推算,不能返回來往前“回顧”(即只能next,不能prev)。
好了,闡述這么多,回歸到目的用途上,生成器也是用來生成數據的--按照既定的某種算法不斷生成后續的新的數據,直到不再循環(調用新數據)或者說滿足了某個指定的條件后結束。
2.3 生成器的構造方式
可通過以下兩種方式來構造生成器:
- 通過類似列表生成式的方式來構造,把列表生成式中的列表符號[]替換為函數符號()即可(指定的算法需要通過函數來定義呀)
- 使用包含yield關鍵字的函數來構造
如果循環的邏輯算法比較簡單,可直接使用第一種方式,反之算法比較復雜時(某些列表很難用列表生成式寫出來,但是用函數就很容易實現),就只能通過第二種包含yield的函數(這是生成器與普通函數在形式上的唯一區別)來構造生成器了。
還是通過實例來形象理解吧。
(1) 通過類似列表生成式的方式來構造
1 gen1 = ( n for n in range(11) if n % 2 == 0) 2 print(type(gen1)) 3 print(gen1) 4 5 輸出: 6 <class 'generator'> 7 <generator object <genexpr> at 0x0000000001DF0830>
(2) 通過函數來構造
1 def gen2(): 2 for i in range(11): 3 if i % 2 == 0: 4 yield n 5 print(type(gen2())) 6 print(gen2()) 7 8 輸出: 9 <class 'generator'> 10 <generator object gen2 at 0x0000000001E20830>
從這里可以看出對於比較簡單的推算算法,如果通過類似列表生成式的方式和函數都可以構造,那么用類似列表生成式的方式顯然更簡單快捷。
但對於類似下面復雜的情形,我們只能選擇通過函數來構造生成器:
比如,著名的斐波拉契數列(Fibonacci),除第一個和第二個數外,任意一個數都可由前兩個數相加得到:1, 1, 2, 3, 5, 8, 13, 21, 34, ...
斐波拉契數列用列表生成式寫不出來,但是,用函數把它打印出來卻很容易:
1 def fib(max): 2 n, a, b = 0, 0, 1 3 while n < max: 4 print(b) 5 a, b = b, a + b 6 n = n + 1 7 return 'done' 8 9 '''注意,賦值語句:a, b = b, a + b相當於: 10 t = (b, a + b) # t是一個tuple 11 a = t[0] 12 b = t[1] 13 但不必顯式寫出臨時變量t就可以賦值。 14 ''' 15
仔細觀察,可以看出,fib
函數實際上是定義了斐波拉契數列的推算規則,可以從第一個元素開始,推算出后續任意的元素,這種邏輯其實非常類似generator。
這里把fib函數轉換成生成器:
1 def fib(max): 2 n, a, b = 0, 0, 1 3 while n < max: 4 yield b 5 a, b = b, a + b 6 n = n + 1 7 return 'done'
注意這里把普通的函數轉換成生成器的時候只有一個變化: 把原來的print轉換成了yield.
一旦一個函數定義中包含yield
關鍵字,那么這個函數就不再是一個普通函數,而是一個generator:
<generator object fib at 0x0000000001DF0830>
2.4 訪問生成器的數據
可通過以下兩種方式來訪問生成器中的數據:
- 通過__next__()方法
1 gen1 = ( n for n in range(11) if n % 2 == 0) 2 print(gen1.__next__()) 3 print(gen1.__next__()) 4 print(gen1.__next__()) 5 print(gen1.__next__()) 6 print(gen1.__next__()) 7 print(gen1.__next__()) 8 9 輸出: 10 0 11 2 12 4 13 6 14 8 15 10
1 def fib(max): 2 n, a, b = 0, 0, 1 3 while n < max: 4 yield b 5 a, b = b, a + b 6 n = n + 1 7 return 'done' 8 f = fib(10) 9 print(f.__next__()) 10 print(f.__next__()) 11 print(f.__next__()) 12 print(f.__next__()) 13 print(f.__next__()) 14 15 輸出: 16 1 17 1 18 2 19 3 20 5
- 通過for循環去迭代
上文__next__()方法訪問生成器的數據例子中,訪問數據很麻煩,只能一個個地去next,對於可生成很多個元素的生成器而言,需要獲取所有的元素時,__next__()顯得無能為力。此時簡單的for循環即可搞定。
例 1:
1 gen1 = ( n for n in range(11) if n % 2 == 0) 2 for i in gen1: 3 print(i) 4 5 輸出: 6 0 7 2 8 4 9 6 10 8 11 10
例2:
1 def fib(max): 2 n, a, b = 0, 0, 1 3 while n < max: 4 yield b 5 a, b = b, a + b 6 n = n + 1 7 return 'done' 8 f = fib(10) 9 for i in f: 10 print(i) 11 12 輸出: 13 1 14 1 15 2 16 3 17 5 18 8 19 13 20 21 21 34 22 55
2.5 關於StopIteration
上文中在訪問生成器的數據時,沒有闡述StopIteration,這里單獨列出來。
當生成器中的數據被訪問完畢后仍然嘗試訪問時,會拋出StopIteration異常,意思是不能再迭代了。通過__next__()方法訪問生成器中的數據時可能會觸發該異常,而通過for循環不會產生該異常,原因是for循環訪問時有明確的循環結束條件。
1. 列表生成式生成器拋出StopIteration異常:
1 gen1 = ( n for n in range(11) if n % 2 == 0) 2 print(gen1.__next__()) 3 print(gen1.__next__()) 4 print(gen1.__next__()) 5 print(gen1.__next__()) 6 print(gen1.__next__()) 7 print(gen1.__next__()) 8 print(gen1.__next__()) #不斷地通過__next__()方法嘗試訪問生成器的數據,直到“越界” 9 10 程序輸出: 11 0 12 2 13 4 14 6 15 8 16 10 17 Traceback (most recent call last): 18 File "D:/python/S13/Day4/1.py", line 26, in <module> 19 print(gen1.__next__()) 20 StopIteration #最后一次嘗試訪問拋出了異常
2. yield 函數生成器拋出StopIteration異常:
1 def fib(max): 2 n, a, b = 0, 0, 1 3 while n < max: 4 yield b 5 a, b = b, a + b 6 n = n + 1 7 return 'done' 8 f = fib(5) 9 for i in range(7): 10 print(f.__next__()) 11 i += 1 12 13 輸出: 14 1 15 1 16 2 17 3 18 5 19 Traceback (most recent call last): 20 File "D:/python/S13/Day4/1.py", line 51, in <module> 21 print(f.__next__()) 22 StopIteration: done
可以看出yield 函數生成器一樣可以拋出StopIteration異常,在引發StopIteration異常后return定義的返回值會打印輸出。換句話說,如果想獲得生成器函數的返回值,只能通過不斷地訪問生成器的數據直到拋出StopIteration。
3. 捕獲StopIteration異常
Python也具備相應的異常處理機制,這里我們來捕獲StopIteration異常:
1 def fib(max): 2 n, a, b = 0, 0, 1 3 while n < max: 4 yield b 5 a, b = b, a + b 6 n = n + 1 7 return 'done' 8 f = fib(5) 9 while True: 10 try: 11 x = f.__next__() 12 print("f:",x) 13 except StopIteration as e: 14 print("Generator return value:",e.value) #當try中預期需要執行的代碼塊執行出錯時,就會執行except中的代碼塊 15 break 16 17 程序輸出: 18 f: 1 19 f: 1 20 f: 2 21 f: 3 22 f: 5 23 Generator return value: done
這里只是簡單演示下如何捕獲StopIteration異常,關於異常處理的更多細節,將在后續的筆記中深入展開。
2.6 yield的特殊性
我們已經知道yield關鍵字可以把一個函數轉換為生成器,yield語句用來代替普通函數中的return來返回結果,我們每嘗試訪問一個生成器中的元素時,如果沒有拋出異常,yield就返回一次結果。這個過程中存在一個特殊性:yield語句每次返回結果后就掛起函數的狀態,以便下次從離開它的地方繼續執行。
聽起來似乎不太好理解,先來一段更詳細的闡述把(引用自 https://www.ibm.com/developerworks/cn/opensource/os-cn-python-yield/?cmp=dwnpr&cpb=dw&ct=dwcon&cr=cn_51CTO_dl&ccy=cn):
簡單地講,yield 的作用就是把一個函數變成一個 generator,帶有 yield 的函數不再是一個普通函數,Python 解釋器會將其視為一個 generator,調用 fab(5) 不會執行 fab 函數,而是返回一個 iterable 對象!在 for 循環執行時,每次循環都會執行 fab 函數內部的代碼,執行到 yield b 時,fab 函數就返回一個迭代值,下次迭代時,代碼從 yield b 的下一條語句繼續執行,而函數的本地變量看起來和上次中斷執行前是完全一樣的,於是函數繼續執行,直到再次遇到 yield。
1 def fib(max): 2 n, a, b = 0, 0, 1 3 while n < max: 4 yield b 5 print("繼續迭代,呵呵") 6 a, b = b, a + b 7 n = n + 1 8 return 'done' 9 f = fib(5) 10 while True: 11 try: 12 x = f.__next__() 13 print("f:", x) 14 print("我已經循環一次了,插播點廣告把") #獲取一次返回值后可以執行其他的任務,下一次迭代后又能繼續回到原來中斷的位置 15 except StopIteration as e: 16 print("Generator return value:",e.value) 17 break 18 19 輸出: 20 f: 1 21 我已經循環一次了,插播點廣告把 22 繼續迭代,呵呵 23 f: 1 24 我已經循環一次了,插播點廣告把 25 繼續迭代,呵呵 26 f: 2 27 我已經循環一次了,插播點廣告把 28 繼續迭代,呵呵 29 f: 3 30 我已經循環一次了,插播點廣告把 31 繼續迭代,呵呵 32 f: 5 33 我已經循環一次了,插播點廣告把 34 繼續迭代,呵呵 35 Generator return value: done
再來一個展示得更清楚的簡單例子:
1 def test(min, max): 2 for n in range(min, max): 3 yield n 4 print('Break point') 5 6 f = test(1, 4) 7 print(f.__next__()) 8 print(f.__next__()) # 執行兩次__next__()方法訪問生成器的元素 9 10 輸出: 11 1 12 Break point #結果只輸出了一個斷點,也就是yield后面的程序,說明只能是第二次訪問元素時輸出的 13 2
上述程序中我們通過兩次執行__next()__方法來訪問生成器的元素,結果只有一個break point的輸出,這是第二次的__next__()訪問的輸出,如果注釋掉第二個next,我們會發現輸出中沒有break point,也就是yield后面的代碼。
這足以說明yield語句執行后程序處於中斷狀態,同時保留了程序執行的狀態和位置,當我們執行其他任務后繼續迭代時程序還能回到之前中斷的狀態和位置,這就給了我們通過生成器進行並行計算的機會,哈哈。
2.7 send方法與生成器並行計算
前文已經提到,可以通過__next__()方法來訪問生成器中的元素,其實質是__next__()方法喚醒了yield(yield返回一次后保持中斷狀態),yield再返回下一次迭代的數據。for循環訪問生成器的數據也可以視為通過循環喚醒yield返回數據。
除此之外生成器中還有一個send()方法可用來喚醒yield,其不同之處在於send()不僅能喚醒yield,而且能給yield傳值(該值將成為當前yield表達式的結果)
注意:
通過send()方法來訪問生成器的元素時,send()方法第一次傳入的參數必須為None,或者在第一次傳入參數之前先調用__next__()方法,以便生成器先進入yield表達式,否則會報錯。
下面通過實例來演示下:
1 def consumer(name): 2 print("%s 准備吃包子啦!"%name) 3 4 while True: 5 baozi = yield 6 print("包子[%s]來了,被[%s]吃了" % (baozi, name)) 7 8 c = consumer("Maxwell") 9 c.__next__() # 不使用__next__()方法會報錯,也可以用c.send(None)替代 10 c.send("肉松餡") # 調用yield,同時給yield傳一個值 11 c.send("韭菜餡") # 再次調用yield,同時給yield傳一個值 12 13 輸出: 14 Maxwell 准備吃包子啦! 15 包子[肉松餡]來了,被[Maxwell]吃了 16 包子[韭菜餡]來了,被[Maxwell]吃了
從上面的示例程序可以看出,原本yield只能返回None,但通過send()方法傳入參數后,該參數直接變成yield的值返回了。
總結下send()和__next__()方法的區別:
1. __next__()方法可以喚醒yield,只能get yield的返回值,只讀訪問生成器當前迭代的返回值;
2.send()方法不能可以喚醒yield,還能傳值給yield,並且set yield的返回值,相當於寫覆蓋方式訪問生成器當前迭代的返回值
3.第一次使用send之前需要確保生成器已經走到yield這一步(即已經中斷過一次),可通過先執行__next__()或send(None)來確保,否則會報錯,而__next__()沒有這個限制。
下面進入生成器通過協程進行並行計算的章節。
先了解下基本理論吧(以下文字引自 http://blog.csdn.net/dutsoft/article/details/54729480):
協程,又稱微線程。英文名Coroutine。
子程序,或者稱為函數,在所有語言中都是層級調用,比如A調用B,B在執行過程中又調用了C,C執行完畢返回,B執行完畢返回,最后是A執行完畢。所以子程序調用是通過棧實現的,一個線程就是執行一個子程序。
協程不同於線程,線程是搶占式的調度,而協程是協同式的調度,協程需要自己做調度。
子程序調用總是一個入口,一次返回,調用順序是明確的。而協程的調用和子程序不同。協程看上去也是子程序,但執行過程中,在子程序內部可中斷,然后轉而執行別的子程序,在適當的時候再返回來接着執行。
協程優勢是極高的執行效率。因為子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。用來執行協程多任務非常合適。
協程沒有線程的安全問題。一個進程可以同時存在多個協程,但是只有一個協程是激活的,而且協程的激活和休眠又程序員通過編程來控制,而不是操作系統控制的。
因為協程是一個線程中執行,那怎么利用多核CPU呢?最簡單的方法是多進程+協程,既充分利用多核,又充分發揮協程的高效率,可獲得極高的性能。
Python對協程的支持是通過generator實現的。在generator中,我們不但可以通過for循環來迭代,還可以不斷調用next()函數獲取由yield語句返回的下一個值。但是Python的yield不但可以返回一個值,它還可以接收調用者發出的參數。
來一個生產者消費者的實際例子:
1 import time 2 3 def consumer(name): 4 print("%s 准備吃包子啦!"%name) 5 6 while True: 7 baozi = yield 8 9 print("包子[%s]來了,被[%s]吃了"%(baozi,name)) 10 11 12 def producer(name): 13 c = consumer("A") 14 c2 = consumer("B") 15 c.__next__() 16 c2.__next__() 17 print("老子准備吃包子啦!") 18 for i in range(5): 19 time.sleep(1) 20 print("做了一個包子,分兩半") 21 c.send(i) 22 c2.send(i) 23 24 producer("Maxwell") 25 26 程序輸出: 27 A 准備吃包子啦! 28 B 准備吃包子啦! 29 老子准備吃包子啦! 30 做了一個包子,分兩半 31 包子[0]來了,被[A]吃了 32 包子[0]來了,被[B]吃了 33 做了一個包子,分兩半 34 包子[1]來了,被[A]吃了 35 包子[1]來了,被[B]吃了 36 做了一個包子,分兩半 37 包子[2]來了,被[A]吃了 38 包子[2]來了,被[B]吃了 39 做了一個包子,分兩半 40 包子[3]來了,被[A]吃了 41 包子[3]來了,被[B]吃了 42 做了一個包子,分兩半 43 包子[4]來了,被[A]吃了 44 包子[4]來了,被[B]吃了
簡單分析下執行過程:消費者其實是一個生成器,生產者做出包子后,通過send方法把值傳遞給消費者並調用切換到消費者執行,消費者又通過yield把結果返回,並回到生產者這里。因此生產者不僅僅是給消費者傳遞包子,還會等包子被吃了的消息返回后再繼續生產下一輪的包子,每次循環是一個輪回,在一個線程內由生成者和消費者相互協作完成,故而稱之為協程。
三、Iterable
可直接用於for循環的對象稱為可迭代對象,即Iterable。
屬於可迭代的數據類型有:
- 集合數據類型:如list、tuple、dict、set、str等
- 生成器(generator)
可通過isinstance()來判斷一個對象是否是Iterable對象(注意后面的判斷類型是Iterable):
1 >>> from collections import Iterable 2 >>> isinstance((),Iterable) 3 True 4 >>> isinstance([],Iterable) 5 True 6 >>> isinstance({},Iterable) 7 True 8 >>> isinstance('python',Iterable) 9 True 10 >>> isinstance((x for x in range(10)),Iterable) 11 True 12 >>> isinstance(100,Iterable) 13 False 14 >>>
四、Iterator
可以被__next()__函數調用並不斷返回下一個值的對象稱為迭代器(Iterator)。生成器符合這一定義,因此生成器也是一種迭代器。
如何理解迭代器(Iterator):
實際上,Python中的Iterator對象表示的是一個數據流,Iterator可以被__next__()函數調用並不斷返回下一個數據,直到沒有數據可以返回時拋出StopIteration
異常錯誤。可以把這個數據流看做一個有序序列,但我們無法提前知道這個序列的長度。同時,Iterator的計算是惰性的,只有通過__next___()函數時才會計算並返回下一個數據。(此段內容來自 這里)
p.s.:生成器完全符合上述特征。
isinstance()也可以用來判斷一個對象是否是迭代器,但需要注意的是后面的判斷類型參數是Iterator:
1 >>> from collections import Iterator 2 >>> list1=['Python', 'Java', 'PHP'] 3 >>> isinstance(list1,Iterator) 4 False 5 >>> print(list1.__next__()) 6 Traceback (most recent call last): 7 File "<stdin>", line 1, in <module> 8 AttributeError: 'list' object has no attribute '__next__' 9 >>> gen1=(x for x in range(11) if x % 2 == 0) 10 >>> print(type(gen1)) 11 <class 'generator'> 12 >>> isinstance(gen1, Iterator) 13 True 14 >>> isinstance('abc', Iterator) 15 False 16 >>> 17
五、Iterable、Iterator與Generator之間的關系
- 生成器對象既是可迭代對象,又是迭代器
生成器對象可直接用於for循環,同時可以被__next()__函數調用並不斷返回下一個值,直到沒有數據可以返回時拋出StopIteration
異常錯誤。因此生成器同時符合可迭代對象和迭代器的定義。 - 迭代器一定是可迭代對象,反之則不一定
迭代器可直接用於for循環,因此一定是可迭代對象,但可迭代對象不一定能被__next()__函數調用並不斷返回下一個值。例如list、dict、str等集合數據類型是可迭代對象,但不能通過__next__()函數調用,所以不是迭代器。但是它們可以通過iter()函數轉換為一個迭代器對象。
1 >>> from collections import Iterator 2 >>> list1=['Python', 'Java', 'PHP'] 3 >>> isinstance(iter(list1),Iterator) 4 True 5 >>> isinstance(list1,Iterator) 6 False 7 >>> print(list1.__next__()) 8 Traceback (most recent call last): 9 File "<stdin>", line 1, in <module> 10 AttributeError: 'list' object has no attribute '__next__' 11 >>>