文章目錄
- 一、疊代器
-
- 檢視一個對象是否是可疊代對象方法
- 自制疊代器
- 斐波那契數列疊代器
- 小結:
- 二、生成器(側重點理清yield,其他的作為補充拓展)
-
- yield 完成多任務
- 清單推導式
- 生成器表達式
- 字典推導式
- 集合推導式
- 斐波那契數列生成器
- 總結:
- 三、greenlet和gevent協程完成多任務
-
- 協程通俗了解
- 協程和線程差異
- greenlet完成多任務
- gevent 完成多任務
- 程序、線程和協程對比
一句話說明什麼是協程:
協程是一種使用者态的輕量級線程,即協程是由使用者程式自己控制排程的。
協程就是告訴Cpython解釋器,你不是nb嗎,不是搞了個GIL鎖嗎,那好,我就自己搞成一個線程讓你去執行,省去你切換線程的時間,我自己切換比你切換要快很多,避免了很多的開銷,對于單線程下,我們不可避免程式中出現io操作,但如果我們能在自己的程式中(即使用者程式級别,而非作業系統級别)控制單線程下的多個任務能在一個任務遇到io阻塞時就切換到另外一個任務去計算,這樣就保證了該線程能夠最大限度地處于就緒态,即随時都可以被cpu執行的狀态,相當于我們在使用者程式級别将自己的io操作最大限度地隐藏起來,進而可以迷惑作業系統,讓其看到:該線程好像是一直在計算,io比較少,進而更多的将cpu的執行權限配置設定給我們的線程。
協程的本質就是在單線程下,由使用者自己控制一個任務遇到io阻塞了就切換另外一個任務去執行,以此來提升效率。
想要了解協程gevent,我們首先要了解greenlet,而要了解greenlet我們就要了解yield,而yield 就在生成器裡,生成器又是特殊的疊代器,接下來,我們就開始從疊代器進行講起。
一、疊代器
疊代是通路集合元素的一種方式。疊代器是一個可以記住周遊的位置的對象。疊代器對象從集合的第一個元素開始通路,直到所有的元素被通路完結束。疊代器隻能往前不會後退。
-
可疊代對象
我們已經知道可以對list、tuple、str等類型的資料使用for…in…的循環文法從其中依次拿到資料進行使用,我們把這樣的過程稱為周遊,也叫疊代。
我們之前⼀直在⽤可疊代對象進⾏疊代操作,那麼到底什麼是可疊代對象. ⾸先我們先回顧⼀下⽬前我們所熟知的可疊代對象有哪些:str, list, tuple, dict, set.
那為什麼我們可以稱他們為可疊代對象呢? 因為他們都遵循了可疊代協定. 什麼是可疊代協定. ⾸先我們先看以下錯誤代碼:
for i in 123:
print(i)
注意看報錯資訊中有這樣⼀句話. ‘int’ object is not iterable . 翻譯過來就是整數類型對象
是不可疊代的。iterable表⽰可疊代的,表⽰可疊代協定. 那麼如何進⾏驗證你的資料類型是否符合可疊代協定?
我們可以通過dir函數來檢視類中定義好的所有⽅法.如:
結果:

檢視一個對象是否是可疊代對象方法
1.用dir()函數在列印結果中. 尋找__iter__ ,如果能找到,那麼這個類的對象就是⼀個可疊代對象.
2.通過isinstance()函數來檢視⼀個對象是什麼類型的如:
l = [1,2,3]
l_iter = l.__iter__()
from collections import Iterable
from collections import Iterator
print(isinstance(l,Iterable)) #True 表明是可疊代的,遵守可疊代協定
print(isinstance(l,Iterator)) #False 表明不是疊代器
print(isinstance(l_iter,Iterator)) #True 表明是疊代器
print(isinstance(l_iter,Iterable)) #True
綜上. 我們可以确定. 如果對象中有__iter__函數. 那麼我們認為這個對象遵守了可疊代協定,就可以擷取到相應的疊代器. 這⾥的__iter__是幫助我們擷取到對象的疊代器. 我們使⽤疊代器中的__next__()來擷取到⼀個疊代器中的元素. 那麼我們之前講的for的⼯作原理到底是什麼, 繼續看代碼:
s = "一簾幽夢曉生涼"
c = s.__iter__() # 擷取疊代器
print(c.__next__()) # 使⽤疊代器進⾏疊代. 擷取⼀個元素 一
print(c.__next__()) # 簾
print(c.__next__()) # 幽
print(c.__next__()) # 夢
print(c.__next__()) # 曉
print(c.__next__()) # 生
print(c.__next__()) # 涼
print(c.__next__()) # StopIteration
結果:
for循環的機制:
for i in [1,2,3]:
print(i)
使⽤while循環+疊代器來模拟for循環:
lst = [1,2,3]
lst_iter = lst.__iter__()
while True:
try:
i = lst_iter.__next__()
print(i)
except StopIteration:
break
自制疊代器
好了,相信通過以上講解,我們已經對疊代器有了清晰的了解,那麼我們能不能通過類建立對象做一個疊代器呢?當然可以,我們都知道了 疊代器内部包含__iter__() 同時包含__next__(). ,下面我們開始嘗試,示例代碼:
from collections.abc import Iterable,Iterator
import time
class DiyName: # 類A
def __init__(self):
self.name = []
self.start_num = 0
def add_name(self,name):
self.name.append(name)
def __iter__(self):
return self # 傳回疊代器對象,将本身自己做成疊代器就傳回自己
def __next__(self): # for循環其實就是根據這個方法去取值
if self.start_num < len(self.name): # 索引值不能大于清單的長度
a = self.name[self.start_num]
self.start_num += 1
return a # 取出類A登記名單
else: # 停止for循環無腦取值的方式
raise StopIteration
man1 = DiyName()
man1.add_name("王一")
man1.add_name("陳二")
man1.add_name("張三")
print('man1是否是可疊代的:',isinstance(man1,Iterable))
print('man1是否是疊代器:',isinstance(man1,Iterator))
for i in man1:
time.sleep(1)
print(i)
# # 第一步 判斷是否是一個可疊代對象,__iter__
# # 第二步 iter(man1) >>> 得到該執行個體對象的__iter__的傳回值
# # 第三步 傳回的這個東西 是否是一個 疊代器 >>> __iter__,__next__
# # 取值的時候 next()
執行結果:
以上示例代碼沒有什麼難了解的,可能大家對第一行的collections.abc會有疑問,其實這個.abc要不要無所謂,也會拿到結果,但是使用 from collections import Iterable 時
會有如下警告:
DeprecationWarning:
Using or importing the ABCs from 'collections'
instead of from 'collections.abc' is deprecated,
and in 3.8 it willstop working
翻譯過來就是:棄用警告:從collections中導入ABCs已被棄用,并在python3.8中将停止工作,可使用collections.abc代替它進行使用。
由于本人使用的還是python3.7的解釋器,是以需要加上.abc去掉警告。
斐波那契數列疊代器
下面再舉個疊代器的小栗子:
class FibIterator(object):
"""斐波那契數列疊代器"""
def __init__(self, n):
"""
:param n: int, 指明生成數列的前n個數
"""
self.n = n
# current用來儲存目前生成到數列中的第幾個數了
self.current = 0
# num1用來儲存前前一個數,初始值為數列中的第一個數0
self.num1 = 0
# num2用來儲存前一個數,初始值為數列中的第二個數1
self.num2 = 1
# 0 ,1 ,1, 2 ,3, 5,8,13
def __next__(self):
"""被next()函數調用來擷取下一個數"""
if self.current < self.n:
num = self.num1
self.num1, self.num2 = self.num2, self.num1 + self.num2
self.current += 1
return num
else:
raise StopIteration
def __iter__(self):
"""疊代器的__iter__傳回自身即可"""
return self
if __name__ == '__main__':
fib = FibIterator(10)
for num in fib:
print(num, end=" ")
執行結果:
小結:
Iterable: 可疊代對象. 内部包含__iter__()函數
Iterator: 疊代器. 内部包含__iter__() 同時包含__next__().
疊代器的特點:
- 節省記憶體.
- 惰性機制
- 不能反複, 隻能向下執⾏.
我們可以把要疊代的内容當成⼦彈,然後擷取到疊代器__iter__(), 就把⼦彈都裝在彈夾中,然後發射就是__next__()把每⼀個⼦彈(元素)打出來。即 for循環的時候,⼀開始時是__iter__()來擷取疊代器,後⾯每次擷取元素都是通過__next__()來完成的,當程式遇到StopIteration将結束循環。
二、生成器(側重點理清yield,其他的作為補充拓展)
什麼是生成器?⽣成器實質就是疊代器.
在python中有三種⽅式來擷取⽣成器:
- 通過⽣成器函數
- 通過各種推導式來實作⽣成器
-
通過資料的轉換也可以擷取⽣成器
⾸先, 我們先看⼀個很簡單的函數:
def func():
print("枕上詩書閑處好,")
return "門前風景雨來佳。"
ret = func()
print(ret)
将函數中的return換成yield就是⽣成器:
def func():
print("枕上詩書閑處好,")
yield "門前風景雨來佳。"
ret = func()
print(ret)
運⾏的結果和上⾯不⼀樣。為什麼呢?由于函數中存在了yield,那麼這個函數就是⼀個⽣成器函數. 這個時候,我們再執⾏這個函數的時候就不再是函數的執⾏了,⽽是擷取這個⽣成器.
如何使⽤呢? 想想疊代器. ⽣成器的本質是疊代器. 是以. 我們可以直接執⾏__next__()來執⾏以下⽣成器:
def func():
print("枕上詩書閑處好,")
yield "門前風景雨來佳。"
gener = func() # 這個時候函數不會執⾏,⽽是擷取到⽣成器
ret = gener.__next__() # 這個時候函數才會執⾏,yield的作⽤和return⼀樣也是傳回資料
print(ret)
相同效果:
我們可以看到, yield和return的效果是⼀樣的。有什麼差別呢? yield是分段來執⾏⼀個函數, return則直接停⽌執⾏函數。
def func():
print("1")
yield 2
print("3")
yield 4
gener = func()
ret = gener.__next__()
print(ret) # 1 2
ret2 = gener.__next__()
print(ret2) # 3 4
ret3 = gener.__next__() # 最後⼀個yield執⾏完畢再次__next__()程式報錯
print(ret3)
結果:
好了,費盡心機提生成器,那為什麼要用⽣成器呢? 還是疊代器的特點:節省記憶體。使⽤⽣成器⼀次就⼀個,⽤多少⽣成多少。⽣成器是⼀個⼀個的指向下⼀個,不會回去, next()到哪, 指針就指到哪⼉,下⼀次繼續擷取指針指向的值。
我們除了可以使用next()函數來喚醒生成器繼續執行外,還可以使用send()函數來喚醒執行。使用send()函數的一個好處是可以在喚醒的同時向斷點處傳入一個附加資料。
接下來我們來看send⽅法, send和__next__()⼀樣都可以讓⽣成器執⾏到下⼀個yield.
def gan():
print("今晚幹什麼呀?")
a = yield "看書"
print("a=",a)
b = yield "寫部落格"
print("b=",b)
c = yield "撸代碼"
print("c=",c)
yield "GAME OVER"
gen = gan() # 擷取⽣成器
ret1 = gen.__next__()
print(ret1)
ret2 = gen.send("看電影")
print(ret2)
ret3 = gen.send("打遊戲")
print(ret3)
ret4 = gen.send("睡覺")
print(ret4)
執行結果:
仔細看結果,一開始是不是想當然的認為a = 看書,b = 寫部落格,c = 撸代碼?
這裡我們就需要關注send和__next__()差別:
- send和next()都是讓⽣成器向下走⼀次
send可以給上⼀個yield的位置傳遞值, 不能給最後⼀個yield發送值,在第⼀次執⾏⽣成器代碼的時候不能使⽤send()
看過後問題是不是迎刃而解?咱們用send給第一個yield 位置傳遞了看電影給a,是以會顯示a = 看電影 其他同理。
⽣成器可以使⽤for循環來循環擷取内部的元素:
def func():
print(1)
yield 2
print(3)
yield 4
print(5)
yield 6
gen = func()
for i in gen:
print(i)
執行結果:
yield 完成多任務
import time
def sing(): # 生成器模闆
while True:
print('***我在唱歌***')
time.sleep(1) # alt
yield #暫停挂起的機制
def dance():
while True:
print('---我在跳舞---')
time.sleep(1)
yield
# Thread Process 協程
def main():
t1 = sing()
t2 = dance()
while True:
try:
next(t1) # 喚醒生成器
next(t2)
except Exception:
break
if __name__ == '__main__':
main()
執行效果:
利用yield的暫停挂起機制起到多任務的效果,實際上這也是并發是假的多任務。
清單推導式
生成器的表現形式之一: 清單推導式 ,⾸先我們先看⼀下這樣的代碼, 給出⼀個清單, 通過循環, 向清單中添加1-10 :
lst = []
for i in range(1, 11):
lst.append(i)
print(lst)
替換成清單推導式:
lst = [i for i in range(1, 11)]
print(lst)
清單推導式是通過⼀⾏來建構你要的清單, 清單推導式看起來代碼簡單,但是出現錯誤之後很難排查。
清單推導式的常用寫法: [ 結果 for 變量 in 可疊代對象]
從python1到python10:
lst = ['python%s' % i for i in range(1,11)]
print(lst)
篩選模式:
[ 結果 for 變量 in 可疊代對象 if 條件 ]
# 擷取1-100内所有的奇數
lst = [i for i in range(1, 100) if i % 2 != 0]
print(lst)
結果:
生成器表達式
⽣成器表達式和清單推導式的文法基本上是⼀樣的. 隻是把[]替換成()
gen = (i for i in range(10))
print(gen)
列印的結果就是⼀個⽣成器對象,我們可以使⽤for循環來循環這個⽣成器:
gen = ("我第%s次寫詩" % i for i in range(10))
for i in gen:
print(i)
結果:
⽣成器表達式也可以進⾏篩選:
# 擷取1-100内能被3整除的數
gen = (i for i in range(1,100) if i % 3 == 0)
for num in gen:
print(num)
# 100以内能被3整除的數的平⽅
gen = (i**2 for i in range(100) if i % 3 == 0)
for num in gen:
print(num)
# 尋找名字中帶有兩個e的⼈的名字
names = [['Tom', 'Billy', 'Jefferson','Joe'],['Alice', 'Jill', 'Ana', 'Wendy', 'Jennifer']]
# 不⽤推導式和表達式
result = []
for first in names:
for name in first:
if name.count("e") >= 2:
result.append(name)
print(result)
# 利用推導式
gen = (name for first in names for name in first if name.count("e") >= 2)
for name in gen:
print(name)
⽣成器表達式和清單推導式的差別:
1. 清單推導式比較耗記憶體,⼀次性加載。 ⽣成器表達式幾乎不占⽤記憶體. 使⽤的時候才配置設定和使⽤記憶體。
2. 得到的值不⼀樣。清單推導式得到的是⼀個清單. ⽣成器表達式擷取的是⼀個⽣成器對象
生成器的惰性機制:== ⽣成器隻有在通路的時候才取值. 說⽩了就是你找他要他才給你值,不找他要他是不會執⾏的.==
def func():
print(111)
yield 222
g = func() # ⽣成器g
g1 = (i for i in g) # ⽣成器g1. 但是g1的資料來源于g
g2 = (i for i in g1) # ⽣成器g2. 來源g1
print(list(g)) # 擷取g中的資料. 這時func()才會被執⾏. 列印111.擷取到222. g完畢.
print(list(g1)) # 擷取g1中的資料. g1的資料來源是g. 但是g已經取完了. g1 也就沒有資料了
print(list(g2)) # 和g1同理
執行結果:
好好捋捋,了解那句“要值的時候才拿值”
字典推導式
# 把字典中的key和value互換
dic = {'a': 1, 'b': '2'}
new_dic = {dic[key]: key for key in dic}
print(new_dic)
# 在以下list中. 從lst1中擷取的資料和lst2中相對應的位置的資料組成⼀個新字典
lst1 = ['青花瓷', '天下', '詩仙']
lst2 = ['周傑倫', '張傑', '李白']
dic = {lst2[i]: lst1[i] for i in range(len(lst1))}
print(dic)
執行結果:
集合推導式
集合推導式可以幫我們直接⽣成⼀個集合,集合的特點: ⽆序, 不重複. 是以集合推導式⾃帶去重功能:
lst = [1, -1, 8, -8, 12]
# 絕對值去重
s = {abs(i) for i in lst}
print(s)
程式執行結果:
通過前面講的,咱們知道函數裡面有yield,那麼函數就是一個生成器。
斐波那契數列生成器
咱們前面講過用疊代器求斐波那契數列,這次咱們試着用生成器做一下:
def fei_bo(c):
a, b = 0 ,1
start_num = 0
while start_num < c: # c的值就是我們取值斐波那契的範圍
yield a # 不會停止整個代碼塊的運作 暫停挂起的機制
a, b = b , a + b
start_num += 1
fei_bo1 = fei_bo(10)
# 用for 循環将生成器的前10個斐波那契數列列印出來
for i in fei_bo1:
print(i,end=',')
執行結果:
總結:
推導式有, 清單推導式, 字典推導式, 集合推導式, 沒有元組推導式
⽣成器表達式: (結果 for 變量 in 可疊代對象 if 條件篩選)
⽣成器表達式可以直接擷取到⽣成器對象. ⽣成器對象可以直接進⾏for循環,⽣成器具有惰性機制。。。
有人就問了:不是為了講yield嗎?為啥還那麼嚴肅地附帶講那麼多?
都是知識點啊,技多不壓身,你懂的多你就更牛逼不是嗎?。。。
三、greenlet和gevent協程完成多任務
協程通俗了解
在一個線程中的某個函數,可以在任何地方儲存目前函數的一些臨時變量等資訊,然後切換到另外一個函數中執行,注意不是通過調用函數的方式做到的,并且切換的次數以及什麼時候再切換到原來的函數都由開發者自己确定。
協程和線程差異
在實作多任務時, 線程切換從系統層面遠不止儲存和恢複 CPU上下文這麼簡單。 作業系統為了程式運作的高效性每個線程都有自己緩存Cache等等資料,作業系統還會幫你做這些資料的恢複操作。 是以線程的切換非常耗性能。但是協程的切換隻是單純的操作CPU的上下文,是以一秒鐘切換個上百萬次系統都抗的住。
greenlet完成多任務
為了更好使用協程來完成多任務,python中的greenlet子產品對其封裝,進而使得切換任務變的更加簡單,程式示例:
# 導入的是greentlet子產品裡面的greenlet類
from greenlet import greenlet
import time
def sing():
while True:
print('***我在唱歌***')
time.sleep(0.5)
g2.switch()
def dance():
while True:
print('---我在跳舞---')
time.sleep(0.5)
g1.switch()
g1 = greenlet(sing)
g2 = greenlet(dance)
def main():
g1.switch() # 切換的方法
if __name__ == '__main__':
main()
效果:
gevent 完成多任務
greenlet隻是提供了一種比generator更加便捷的切換方式,當切到一個任務執行時如果遇到io,那就原地阻塞,仍然是沒有解決遇到IO自動切換來提升效率的問題。greenlet已經實作了協程,但是這個還得人工切換,是不是覺得太麻煩了,不要着急,python還有一個比greenlet更強大的并且能夠自動切換任務的子產品gevent
gevent原理是當一個greenlet遇到IO(指的是input output 輸入輸出,比如網絡、檔案操作等)操作時,比如通路網絡,就自動切換到其他的greenlet,等到IO操作完成,再在适當的時候切換回來繼續執行。
由于IO操作非常耗時,經常使程式處于等待狀态,有了gevent為我們自動切換協程,就保證總有greenlet在運作,而不是等待IO
import gevent
import time
def sing():
for i in range(1,4):
print('***我在唱第%s首歌***' % i)
# time.sleep(0.5) 不能用,用了沒效果,gevent有自己得sleep方法
gevent.sleep(0.5)
def dance():
for i in range(1,4):
print('---我在跳第%s支舞---' % i)
# time.sleep(0.5)
gevent.sleep(0.5)
g1 = gevent.spawn(sing)
g2 = gevent.spawn(dance)
def main():
g1.join()
g2.join()
if __name__ == '__main__':
main()
效果:
如果将gevent.sleep(0.5)換成 time.sleep(0.5):
可以看到用time.sleep()在gevent中完全達不到多任務的效果,有人說我已經用習慣了time,那有沒有辦法可以使用time依然可以完成多任務呢?當然有,這種方法我們稱之為打更新檔,程式示例:
# 打更新檔
import gevent
import time
from gevent import monkey
monkey.patch_all()
def sing():
for i in range(1, 4):
print('***我在唱第%s首歌***' % i)
time.sleep(0.5) # >>>實際上底層它最終也是指向 gevent.sleep()
def dance():
for i in range(1, 4):
print('---我在跳第%s隻舞---' % i)
time.sleep(0.5)
g1 = gevent.spawn(sing)
g2 = gevent.spawn(dance)
def main():
g1.join()
g2.join()
if __name__ == '__main__':
main()
效果:
可以這樣記:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到檔案的開頭。
其實代碼我們還是可以稍作一下優化,gevent有一個joinall方法,可以把對象放進一個清單傳入:
import gevent
import time
from gevent import monkey
monkey.patch_all()
def sing():
for i in range(1, 4):
print('***我在唱第%s首歌***' % i)
time.sleep(0.5) # >>>實際上它最終也是指向 gevent.sleep()
def dance():
for i in range(1, 4):
print('---我在跳第%s隻舞---' % i)
time.sleep(0.5)
def main():
gevent.joinall([
gevent.spawn(sing),
gevent.spawn(dance)
])
if __name__ == '__main__':
main()
效果:
程序、線程和協程對比
上圖截自:python中程序、線程、協程比較
通俗了解:
一條流水線及其生産資料為一個程序
流水線上的勞工為單個線程
給閑時的勞工配置設定新的任務, 這個概念就是協程(gevent)
多程序:多條流水線(multiprocessing)
多線程:一條流水線上的多個勞工 (threading)
一般在工作中我們都是程序+線程+協程的方式來實作并發,以達到最好的并發效果,如果是4核的cpu,一般起5個程序,每個程序中20個線程(5倍cpu數量),每個線程可以起500個協程,大規模爬取頁面的時候,等待網絡延遲的時間的時候,我們就可以用協程去實作并發。 并發數量 = 5 * 20 * 500 = 50000個并發,這是一般一個4cpu的機器最大的并發數,nginx在負載均衡的時候最大承載量就是5w個。