一、總體內(nèi)容
1.1虫给、協(xié)程的介紹
1.2藤抡、迭代器以及迭代器的應(yīng)用
1.3、生成器(生成器與迭代器保存的都是生成數(shù)據(jù)的代碼抹估,而不是數(shù)據(jù))
1.4缠黍、gevent 來實現(xiàn)一個?圖片下載器?&?視頻下載器
二、協(xié)程介紹
2.1药蜻、協(xié)程是什么瓷式?
協(xié)程,又稱微線程语泽,纖程贸典。英文名Coroutine:英[k?ru:'ti:n]。
協(xié)程是python個中另外一種實現(xiàn)多任務(wù)的方式湿弦,只不過比線程更小占用更小執(zhí)行單元(理解為需要的資源)瓤漏。 為啥說它是一個執(zhí)行單元,因為它自帶CPU上下文颊埃。這樣只要在合適的時機蔬充, 我們可以把一個協(xié)程 切換到另一個協(xié)程。 只要這個過程中保存或恢復 CPU上下文那么程序還是可以運行的班利。
通俗的理解:在一個線程中的某個函數(shù)饥漫,可以在任何地方保存當前函數(shù)的一些臨時變量等信息,然后切換到另外一個函數(shù)中執(zhí)行罗标,注意不是通過調(diào)用函數(shù)的方式做到的庸队,并且切換的次數(shù)以及什么時候再切換到原來的函數(shù)都由開發(fā)者自己確定积蜻。
2.2、協(xié)程和線程差異
在實現(xiàn)多任務(wù)時, 線程切換從系統(tǒng)層面遠不止保存和恢復 CPU上下文這么簡單彻消。 操作系統(tǒng)為了程序運行的高效性每個線程都有自己緩存Cache等等數(shù)據(jù)竿拆,操作系統(tǒng)還會幫你做這些數(shù)據(jù)的恢復操作。 所以線程的切換非常耗性能宾尚。但是協(xié)程的切換只是單純的操作CPU的上下文丙笋,所以一秒鐘切換個上百萬次系統(tǒng)都抗的住。
2.3煌贴、簡單實現(xiàn)協(xié)程(暫時用yield,后面我們主要用?gevent?來實現(xiàn)?協(xié)程)
import time
def work1():
? ? ? while True:
? ? ? ? ? print("----work1---")
? ? ? ? ? yield
? ? ? ? ? time.sleep(0.5)
def work2():
? ? ? while True:
? ? ? ? ? print("----work2---")
? ? ? ? ? yield
? ? ? ? ? time.sleep(0.5)
def main():
? ? ? w1 = work1()
? ? ? w2 = work2()
? ? ? while True:
? ? ? ? ? next(w1)
? ? ? ? ? next(w2)
if __name__ == "__main__":
? ? ? main()
運行結(jié)果:
---work1---
----work2---
----work1---
----work2---
----work1---
----work2---
---------等等-------
三御板、迭代器
3.1、迭代器的介紹
迭代是訪問集合元素的一種方式牛郑。迭代器是一個可以記住遍歷的位置的對象怠肋。迭代器對象從集合的第一個元素開始訪問,直到所有的元素被訪問完結(jié)束淹朋。迭代器只能往前不會后退笙各。
3.2、可迭代對象
我們已經(jīng)知道可以對list础芍、tuple酪惭、str等類型的數(shù)據(jù)使用for...in...的循環(huán)語法從其中依次拿到數(shù)據(jù)進行使用,我們把這樣的過程稱為遍歷者甲,也叫迭代。是否所有的數(shù)據(jù)類型都可以放到for...in...的語句中砌创,然后讓for...in...每次從中取出一條數(shù)據(jù)供我們使用虏缸,即供我們迭代嗎?
打印結(jié)果是:從下面可以看出不可迭代
TypeError:'MyList'objectisnot iterable
我們自定義了一個容器類型MyList嫩实,在將一個存放了多個數(shù)據(jù)的MyList對象放到for...in...的語句中刽辙,發(fā)現(xiàn)for...in...并不能從中依次取出一條數(shù)據(jù)返回給我們,也就說我們隨便封裝了一個可以存放多條數(shù)據(jù)的類型卻并不能被迭代使用甲献。
我們把可以通過for...in...這類語句迭代讀取一條數(shù)據(jù)供我們使用的對象稱之為可迭代對象(Iterable)宰缤。
3.3、如何判斷一個對象是否可以迭代
可以使用?isinstance()判斷一個對象是否是?Iterable對象,如下判斷
3.4晃洒、可迭代對象的本質(zhì)
我們分析對可迭代對象進行迭代使用的過程慨灭,發(fā)現(xiàn)每迭代一次(即在for...in...中每循環(huán)一次)都會返回對象中的下一條數(shù)據(jù),一直向后讀取數(shù)據(jù)直到迭代了所有數(shù)據(jù)后結(jié)束球及。那么氧骤,在這個過程中就應(yīng)該有一個“人”去記錄每次訪問到了第幾條數(shù)據(jù),以便每次迭代都可以返回下一條數(shù)據(jù)吃引。我們把這個能幫助我們進行數(shù)據(jù)迭代的“人”稱為迭代器(Iterator:[?t?'re?t?])筹陵。
可迭代對象的本質(zhì) 就是可以向我們提供一個這樣的中間“人”即迭代器幫助我們對其進行迭代遍歷使用刽锤。
可迭代對象通過iter方法向我們提供一個迭代器,我們在迭代一個可迭代對象的時候朦佩,實際上就是先獲取該對象提供的一個迭代器并思,然后通過這個迭代器來依次獲取對象中的每一個數(shù)據(jù).
那么也就是說,一個具備了__iter__方法的對象语稠,就是一個可迭代對象宋彼。
在一個類里面加上 __iter__就可以使這個類成為可迭代的對象
打印結(jié)果是:True, 這回測試發(fā)現(xiàn)添加了__iter__方法的mylist對象已經(jīng)是一個?可迭代對象?了
3.5、 iter()函數(shù)與next()函數(shù)
list颅筋、tuple等都是可迭代對象宙暇,我們可以通過iter()函數(shù)獲取這些可迭代對象的迭代器。然后我們可以對獲取到的迭代器不斷使用next()函數(shù)來獲取下一條數(shù)據(jù)议泵。iter()函數(shù)實際上就是調(diào)用了可迭代對象的__iter__方法占贫。
注意,當我們已經(jīng)迭代完最后一個數(shù)據(jù)之后先口,再次調(diào)用next()函數(shù)會拋出StopIteration的異常型奥,來告訴我們所有數(shù)據(jù)都已迭代完成,不用再執(zhí)行next()函數(shù)了碉京。
3.6厢汹、如何判斷一個對象是否是迭代器
可以使用 isinstance() 判斷一個對象是否是 Iterator 對象:
3.7、迭代器Iterator
通過上面的分析谐宙,我們已經(jīng)知道烫葬,迭代器是用來幫助我們記錄每次迭代訪問到的位置,當我們對迭代器使用next()函數(shù)的時候凡蜻,迭代器會向我們返回它所記錄位置的下一個位置的數(shù)據(jù)搭综。實際上,在使用next()函數(shù)的時候划栓,調(diào)用的就是迭代器對象的__next__方法(Python3中是對象的__next__方法兑巾,Python2中是對象的next()方法)。所以忠荞,我們要想構(gòu)造一個迭代器蒋歌,就要實現(xiàn)它的__next__方法。但這還不夠委煤,python要求迭代器本身也是可迭代的堂油,所以我們還要為迭代器實現(xiàn)__iter__方法,而__iter__方法要返回一個迭代器素标,迭代器自身正是一個迭代器称诗,所以迭代器的__iter__方法返回自身即可。
from collections import Iterator
from collections import Iterable
class MyList(object):
? ? ? def __init__(self):
? ? ? ? ? self.name_list = []
? ? ? ? ? self.all_count = 0
? ? ? def add_name(self,temp):
? ? ? ? ? self.name_list.append(temp)
? ? ? def __iter__(self):
? ? ? ? ? return self
? ? ? def __next__(self):
? ? ? ? ? ? if self.all_count < len(self.name_list):
? ? ? ? ? ? ? ? ? ? ret = self.name_list[self.all_count]
? ? ? ? ? ? ? ? ? ? self.all_count += 1
? ? ? ? ? ? ? ? ? ? return ret
? ? ? ? ? ? else:
? ? ? ? ? ? ? ? ? ? raise StopIteration
my_list = MyList()
my_list.add_name("張三")
my_list.add_name("李四")
my_list.add_name("王五")
print("判斷my_list是否是可以迭代的對象:",isinstance(my_list,Iterable))
my_list_iterator = iter(my_list)
print("判斷my_list_iterator是否是迭代器:",isinstance(my_list_iterator,Iterator))
for name in my_list:
? ? ? print(name)
提示:一個實現(xiàn)了__iter__方法和__next__方法的對象头遭,就是迭代器寓免。
3.8癣诱、for...in...循環(huán)的本質(zhì)
for item in Iterable 循環(huán)的本質(zhì)就是先通過iter()函數(shù)獲取可迭代對象Iterable的迭代器,然后對獲取到的迭代器不斷調(diào)用next()方法來獲取下一個值并將其賦值給item袜香,當遇到StopIteration的異常后循環(huán)結(jié)束撕予。
3.9、迭代器的應(yīng)用場景
我們發(fā)現(xiàn)迭代器最核心的功能就是可以通過next()函數(shù)的調(diào)用來返回下一個數(shù)據(jù)值蜈首。如果每次返回的數(shù)據(jù)值不是在一個已有的數(shù)據(jù)集合中讀取的实抡,而是通過程序按照一定的規(guī)律計算生成的,那么也就意味著可以不用再依賴一個已有的數(shù)據(jù)集合欢策,也就是說不用再將所有要迭代的數(shù)據(jù)都一次性緩存下來供后續(xù)依次讀取吆寨,這樣可以節(jié)省大量的存儲(內(nèi)存)空間。
舉例:比如踩寇,數(shù)學中有個著名的斐波拉契數(shù)列(Fibonacci)啄清,數(shù)列中第一個數(shù)為0,第二個數(shù)為1俺孙,其后的每一個數(shù)都可由前兩個數(shù)相加得到:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
class FibIterator(object):
? ? ? ? """斐波那契數(shù)列迭代器"""
? ? ? def __init__(self,num):
? ? ? ? ? ? """
? ? ? ? ? ? :param num: 指明生成數(shù)列的前num個數(shù)
? ? ? ? ? ? """
? ? ? ? ? ? self.all_num = num
? ? ? ? ? ? # 用來保存當前生成到數(shù)列中的第幾個數(shù)了
? ? ? ? ? ? self.num = 0
? ? ? ? ? ? # 用來保存前前一個數(shù)辣卒,初始值為數(shù)列中的第一個數(shù)0
? ? ? ? ? ? self.start_a = 0
? ? ? ? ? ? # 用來保存前后一個數(shù),初始值為數(shù)列中的第二個數(shù)1
? ? ? ? ? ? self.start_b = 1
? ? ? def __iter__(self):
? ? ? ? ? ? # """迭代器的__iter__返回自身即可"""
? ? ? ? ? ? return self
? ? ? def __next__(self):
? ? ? ? ? ? # """被next()函數(shù)調(diào)用來獲取下一個數(shù)"""
? ? ? ? ? ? if self.num < self.all_num:
? ? ? ? ? ? ? ? ? self.num += 1
? ? ? ? ? ? ? ? ? retValue = self.start_a
? ? ? ? ? ? ? ? ? self.start_a,self.start_b = self.start_b,self.start_a+self.start_b
? ? ? ? ? ? ? ? ? return self.start_a
? ? ? ? ? ? else:
? ? ? ? ? ? ? ? ? raise StopIteration
fibIterator = FibIterator(20)
for num in fibIterator:
? ? ? ? print(num)
3.10睛榄、并不是只有for循環(huán)能接收可迭代對象
除了for循環(huán)能接收可迭代對象荣茫,list、tuple等也能接收场靴。
3.11啡莉、以后再見到?元組轉(zhuǎn)列表,列表轉(zhuǎn)元組旨剥,不要再認為是簡簡單單的轉(zhuǎn)換了票罐;比如下面的例子
四、生成器
4.1泞边、生成器介紹
利用迭代器,我們可以在每次迭代獲取數(shù)據(jù)(通過next()方法)時按照特定的規(guī)律進行生成疗杉。但是我們在實現(xiàn)一個迭代器時阵谚,關(guān)于當前迭代到的狀態(tài)需要我們自己記錄,進而才能根據(jù)當前狀態(tài)生成下一個數(shù)據(jù)烟具。為了達到記錄當前狀態(tài)梢什,并配合next()函數(shù)進行迭代使用,我們可以采用更簡便的語法朝聋,即生成器(generator)嗡午。生成器是一類特殊的迭代器。
4.2冀痕、創(chuàng)建生成器方法
創(chuàng)建生成器方法1
創(chuàng)建 L 和 G 的區(qū)別僅在于最外層的 [ ] 和 ( ) 荔睹, L 是一個列表狸演,而 G 是一個生成器。我們可以直接打印出列表L的每一個元素僻他,而對于生成器G宵距,我們可以按照迭代器的使用方法來使用,即可以通過next()函數(shù)吨拗、for循環(huán)满哪、list()等方法使用
創(chuàng)建生成器方法2
generator非常強大。如果推算的算法比較復雜劝篷,用類似列表生成式的for循環(huán)無法實現(xiàn)的時候哨鸭,還可以用函數(shù)來實現(xiàn)。我們?nèi)匀挥枚刑岬降?b>斐波那契數(shù)列來舉例娇妓,回想我們在上一節(jié)用迭代器的實現(xiàn)方式:
在使用生成器實現(xiàn)的方式中像鸡,我們將原本在迭代器next方法中實現(xiàn)的基本邏輯放到一個函數(shù)中來實現(xiàn),但是將每次迭代返回數(shù)值的return換成了yield峡蟋,此時新定義的函數(shù)便不再是函數(shù)坟桅,而是一個生成器了。簡單來說:只要在def中有yield關(guān)鍵字的 就稱為 生成器
此時按照調(diào)用函數(shù)的方式( 案例中為F = fib(5) )使用生成器就不再是執(zhí)行函數(shù)體了蕊蝗,而是會返回一個生成器對象( 案例中為F )仅乓,然后就可以按照使用迭代器的方式來使用生成器了。
但是用for循環(huán)調(diào)用generator時蓬戚,發(fā)現(xiàn)拿不到generator的return語句的返回值夸楣。如果想要拿到返回值,必須捕獲StopIteration錯誤子漩,返回值包含在StopIteration的value中:
總結(jié)
使用了yield關(guān)鍵字的函數(shù)不再是函數(shù)豫喧,而是生成器。(使用了yield的函數(shù)就是生成器)
yield關(guān)鍵字有兩點作用:
保存當前運行狀態(tài)(斷點)幢泼,然后暫停執(zhí)行紧显,即將生成器(函數(shù))掛起
將yield關(guān)鍵字后面表達式的值作為返回值返回,此時可以理解為起到了return的作用
可以使用next()函數(shù)讓生成器從斷點處繼續(xù)執(zhí)行缕棵,即喚醒生成器(函數(shù))
Python3中的生成器可以使用return返回最終運行的返回值孵班,而Python2中的生成器不允許使用return返回一個返回值(即可以使用return從生成器中退出,但return后不能有任何表達式)招驴。
4.3篙程、使用send喚醒
我們除了可以使用next()函數(shù)來喚醒生成器繼續(xù)執(zhí)行外,還可以使用send()函數(shù)來喚醒執(zhí)行别厘。使用send()函數(shù)的一個好處是可以在喚醒的同時向斷點處傳入一個附加數(shù)據(jù)虱饿。
例子:執(zhí)行到y(tǒng)ield時,gen函數(shù)作用暫時保存,返回i的值; temp接收下次c.send("python")氮发,send發(fā)送過來的值渴肉,c.next()等價c.send(None)
4.4、使用__next__()方法(不常使用)
4.5折柠、greenlet
為了更好使用協(xié)程來完成多任務(wù)宾娜,python中的greenlet模塊對其封裝,從而使得切換任務(wù)變的更加簡單
安裝方式
sudo pip3 install greenlet
使用方式
4.6扇售、gevent (重點)
其原理是當一個greenlet遇到IO(指的是input output 輸入輸出前塔,比如網(wǎng)絡(luò)、文件操作等)操作時承冰,比如訪問網(wǎng)絡(luò)华弓,就自動切換到其他的greenlet,等到IO操作完成困乒,再在適當?shù)臅r候切換回來繼續(xù)執(zhí)行寂屏。
由于IO操作非常耗時,經(jīng)常使程序處于等待狀態(tài)娜搂,有了gevent為我們自動切換協(xié)程迁霎,就保證總有g(shù)reenlet在運行,而不是等待IO
安裝 gevent
pip3 install gevent
gevent的使用
運行結(jié)果(可以看到百宇,3個gevent是依次運行而不是交替運行)
gevent切換執(zhí)行
運行結(jié)果: 可以看到是交替執(zhí)行的??
.7考廉、給程序打補丁(如果你在使用gevent的模塊中sleep()用的是time模塊,那么為了使gevent也起作用携御,那么就要進行打補恫痢)
需要加上下面的代碼
# 有耗時操作時需要
monkey.patch_all()? # 將程序中用到的耗時操作的代碼,換為gevent中自己實現(xiàn)的模塊
完整的代碼如下
運行結(jié)果如下:
五啄刹、gevent 來實現(xiàn)一個圖片下載器
5.1涮坐、我們在下載網(wǎng)絡(luò)的時候會用到urllib.request,在用到打開一個鏈接urllib.request.urlopen的時候會報錯:urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1051)>,這是我的報錯
原因是:查找相關(guān)資料后確定為Python 2.7.9 之后版本引入了一個新特性
當你urllib.urlopen一個 https 的時候會驗證一次 SSL 證書
當目標使用的是自簽名的證書時就會爆出一個上面的錯誤,
處理辦法如下:在全局添加下面代碼
importssl ssl._create_default_https_context=ssl._create_unverified_context
作者:IIronMan
鏈接:http://www.reibang.com/p/dabef6d19aed
來源:簡書
著作權(quán)歸作者所有誓军。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)袱讹,非商業(yè)轉(zhuǎn)載請注明出處。
5.2昵时、使用協(xié)程 gevent 實現(xiàn)的圖片下載器
import urllib.request
import ssl
from gevent import monkey
import gevent
monkey.patch_all()
ssl._create_default_https_context = ssl._create_unverified_context
def down_loader_image(image_name,image_url):
? ? ? ? """
? ? ? ? 使用進程 gevent的圖片下載器
? ? ? ? :param image_name: 圖片的名字
? ? ? ? :param image_url: 圖片的url
? ? ? ? """
? ? ? ? req = urllib.request.urlopen(image_url)
? ? ? ? image_content = req.read()
? ? ? ? with open("/Users/wangchong/Desktop/%s"%image_name,"wb") as f:
? ? ? ? ? ? ? f.write(image_content)
def main():
? ? gevent.joinall([
? ? ? ? ? ? ? gevent.spawn(down_loader_image,"20.png","https://rpic.douyucdn.cn/live-cover/appCovers/2018/11/15/5658062_20181115170406_small.jpg"),
? ? ? ? ? ? ? gevent.spawn(down_loader_image,"21.png","https://rpic.douyucdn.cn/live-cover/roomCover/2018/09/27/b0983e7b6267f5b8371106ccf5f113ab_big.jpg")
? ? ? ])
if __name__ == '__main__':
5.3廓译、使用協(xié)程 gevent 實現(xiàn)多個視頻下載
import urllib.request
import ssl
from gevent import monkey
import gevent
#有IO才做時需要這一句,將程序中用到的耗時操作的代碼,換為gevent中自己實現(xiàn)的模塊
monkey.patch_all()
ssl._create_default_https_context = ssl._create_unverified_context
def down_loader_image(video_name,video_url):
? ? ? ? """
? ? ? ? 使用進程 gevent的圖片下載器
? ? ? ? :param video_name: 視頻的名字
? ? ? ? :param video_url: 圖片的url
? ? ? ? """
? ? ? ? req = urllib.request.urlopen(video_url)
? ? ? ? data = req.read()
? ? ? ? with open("/Users/wangchong/Desktop/%s"% video_name,"wb") as f:
? ? ? ? ? ? ? f.write(data)
? ? ? ? print('%d bytes received from %s.' % (len(data), video_url))
def main():
? ? gevent.joinall([
? ? ? ? ? ? ? gevent.spawn(down_loader_image,"1.mp4","http://images.ciotimes.com/2.wx_test.mp4"),
? ? ? ? ? ? ? gevent.spawn(down_loader_image,"2.mp4","http://images.ciotimes.com/3.wx_test.mp4")
? ? ? ])
if __name__ == '__main__':
? ? ? main()
提示 上面的url可以換為自己需要下載視頻债查、音樂、圖片等網(wǎng)址
六瓜挽、總結(jié):進程盹廷、線程、協(xié)程對比
6.1久橙、對進程俄占、線程管怠、協(xié)程三者的描述
有一個老板想要開個工廠進行生產(chǎn)某件商品(例如剪子)
他需要花一些財力物力制作一條生產(chǎn)線,這個生產(chǎn)線上有很多的器件以及材料這些所有的 為了能夠生產(chǎn)剪子而準備的資源稱之為:進程
只有生產(chǎn)線是不能夠進行生產(chǎn)的缸榄,所以老板的找個工人來進行生產(chǎn)渤弛,這個工人能夠利用這些材料最終一步步的將剪子做出來,這個來做事情的工人稱之為:線程
這個老板為了提高生產(chǎn)率甚带,想到3種辦法:
在這條生產(chǎn)線上多招些工人她肯,一起來做剪子,這樣效率是成倍増長鹰贵,即單進程 多線程方式
老板發(fā)現(xiàn)這條生產(chǎn)線上的工人不是越多越好晴氨,因為一條生產(chǎn)線的資源以及材料畢竟有限,所以老板又花了些財力物力購置了另外一條生產(chǎn)線碉输,然后再招些工人這樣效率又再一步提高了籽前,即多進程 多線程方式
老板發(fā)現(xiàn),現(xiàn)在已經(jīng)有了很多條生產(chǎn)線敷钾,并且每條生產(chǎn)線上已經(jīng)有很多工人了(即程序是多進程的枝哄,每個進程中又有多個線程),為了再次提高效率阻荒,老板想了個損招挠锥,規(guī)定:如果某個員工在上班時臨時沒事或者再等待某些條件(比如等待另一個工人生產(chǎn)完謀道工序 之后他才能再次工作) ,那么這個員工就利用這個時間去做其它的事情财松,那么也就是說:如果一個線程等待某些條件瘪贱,可以充分利用這個時間去做其它事情,其實這就是:協(xié)程方式
6.2辆毡、總體描述
進程是資源分配的單位
線程是操作系統(tǒng)調(diào)度的單位
進程切換需要的資源很最大菜秦,效率很低
線程切換需要的資源一般,效率一般(當然了在不考慮GIL的情況下)
協(xié)程切換任務(wù)資源很小舶掖,效率高
多進程球昨、多線程根據(jù)cpu核數(shù)不一樣可能是并行的,但是協(xié)程是在一個線程中 所以是并發(fā)
協(xié)程依賴于線程眨攘,線程依賴于進程主慰,進程一死,協(xié)程必掛
作者:IIronMan
鏈接:http://www.reibang.com/p/dabef6d19aed
來源:簡書
著作權(quán)歸作者所有鲫售。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)共螺,非商業(yè)轉(zhuǎn)載請注明出處。