Python 高階 7
迭代、迭代器、生成器、協程、yield、greenlet、gevent、程序執行緒協程對比、gevent多工圖片下載
1.迭代
<1>迭代的概念
使用for迴圈遍歷取值的過程叫做迭代,比如:使用for迴圈遍歷列表獲取值的過程
for value in [2, 3, 4]:
print(value)
<2>可迭代物件
使用for迴圈遍歷取值的物件叫做可迭代物件, 比如:列表、元組、字典、集合、range、字串
<3>判斷物件是否是可迭代物件
先匯入collections包中的Iterable類
isinstance的使用
isinstance(o, t), object type
isinstance(物件,型別) # 判斷物件是否是指定型別,返回一個布林型別
from collections import Iterable
# 判斷物件是否是指定型別
result = isinstance((3, 5), Iterable)
print("元組是否是可迭代物件:", result)
# 提示: 以後還根據物件判斷是否是其它型別,比如以後可以判斷函式裡面的引數是否是自己想要的型別
result = isinstance(5, int)
print("整數是否是int型別物件:", result)
<4>自定義可迭代物件
自定義可迭代物件: 在類裡面定義__iter__方法建立的物件就是可迭代物件
自定義可迭代型別程式碼
from collections import Iterable
# 自定義可迭代物件: 在類裡面定義__iter__方法建立的物件就是可迭代物件
class MyList(object):
def __init__(self):
self.my_list = list()
# 新增指定元素
def append_item(self, item):
self.my_list.append(item)
def __iter__(self):
# 可迭代物件的本質:遍歷可迭代物件的時候其實獲取的是可迭代物件的迭代器, 然後通過迭代器獲取物件中的資料
# 可迭代物件的本質: 是通過迭代器幫助可迭代物件依次迭代物件中的每一個數據,真正完成獲取資料的操作是通過迭代器完成的
pass
my_list = MyList()
my_list.append_item(1)
my_list.append_item(2)
result = isinstance(my_list, Iterable)
print(result)
for value in my_list:
print(value)
遍歷可迭代物件依次獲取資料需要迭代器
小結
在類裡面提供一個__iter__建立的物件是可迭代物件,可迭代物件是需要迭代器完成資料迭代的。
2.迭代器
<1>自定義迭代器物件
自定義迭代器物件: 在類裡面定義__iter__和__next__方法建立的物件就是迭代器物件
from collections import Iterable
from collections import Iterator
# 自定義可迭代物件: 在類裡面定義__iter__方法建立的物件就是可迭代物件
class MyList(object):
def __init__(self):
self.my_list = list()
# 新增指定元素
def append_item(self, item):
self.my_list.append(item)
def __iter__(self):
# 可迭代物件的本質:遍歷可迭代物件的時候其實獲取的是可迭代物件的迭代器, 然後通過迭代器獲取物件中的資料
my_iterator = MyIterator(self.my_list)
return my_iterator
# 自定義迭代器物件: 在類裡面定義__iter__和__next__方法建立的物件就是迭代器物件
class MyIterator(object):
def __init__(self, my_list):
self.my_list = my_list
# 記錄當前獲取資料的下標
self.current_index = 0
# 判斷當前物件是否是迭代器
result = isinstance(self, Iterator)
print("MyIterator建立的物件是否是迭代器:", result)
def __iter__(self):
return self
# 獲取迭代器中下一個值
def __next__(self):
if self.current_index < len(self.my_list):
self.current_index += 1
return self.my_list[self.current_index - 1]
else:
# 資料取完了,需要丟擲一個停止迭代的異常
raise StopIteration
my_list = MyList()
my_list.append_item(1)
my_list.append_item(2)
result = isinstance(my_list, Iterable)
print(result)
for value in my_list:
print(value)
<2>iter()函式與next()函式
iter函式: 獲取可迭代物件的迭代器,會呼叫可迭代物件身上的__iter__方法
next函式: 獲取迭代器中下一個值,會呼叫迭代器物件身上的__next__方法
# 自定義可迭代物件: 在類裡面定義__iter__方法建立的物件就是可迭代物件
class MyList(object):
def __init__(self):
self.my_list = list()
# 新增指定元素
def append_item(self, item):
self.my_list.append(item)
def __iter__(self):
# 可迭代物件的本質:遍歷可迭代物件的時候其實獲取的是可迭代物件的迭代器, 然後通過迭代器獲取物件中的資料
my_iterator = MyIterator(self.my_list)
return my_iterator
# 自定義迭代器物件: 在類裡面定義__iter__和__next__方法建立的物件就是迭代器物件
# 迭代器是記錄當前資料的位置以便獲取下一個位置的值
class MyIterator(object):
def __init__(self, my_list):
self.my_list = my_list
# 記錄當前獲取資料的下標
self.current_index = 0
def __iter__(self):
return self
# 獲取迭代器中下一個值
def __next__(self):
if self.current_index < len(self.my_list):
self.current_index += 1
return self.my_list[self.current_index - 1]
else:
# 資料取完了,需要丟擲一個停止迭代的異常
raise StopIteration
# 建立了一個自定義的可迭代物件
my_list = MyList()
my_list.append_item(1)
my_list.append_item(2)
# 獲取可迭代物件的迭代器
my_iterator = iter(my_list)
print(my_iterator)
# 獲取迭代器中下一個值
# value = next(my_iterator)
# print(value)
# 迴圈通過迭代器獲取資料
while True:
try:
value = next(my_iterator)
print(value)
except StopIteration as e:
break
<3>for迴圈的本質
遍歷的是可迭代物件
for item in Iterable 迴圈的本質就是先通過iter()函式獲取可迭代物件Iterable的迭代器,然後對獲取到的迭代器不斷呼叫next()方法來獲取下一個值並將其賦值給item,當遇到StopIteration的異常後迴圈結束。
遍歷的是迭代器
for item in Iterator 迴圈的迭代器,不斷呼叫next()方法來獲取下一個值並將其賦值給item,當遇到StopIteration的異常後迴圈結束。
for迴圈內部自動捕獲停止迭代的異常,而while迴圈內部沒有自己捕獲
最終取值操作都是通過迭代器完成的
<4>迭代器的應用場景
我們發現迭代器最核心的功能就是可以通過next()函式的呼叫來返回下一個資料值。如果每次返回的資料值不是在一個已有的資料集合中讀取的,而是通過程式按照一定的規律計算生成的,那麼也就意味著可以不用再依賴一個已有的資料集合,也就是說不用再將所有要迭代的資料都一次性快取下來供後續依次讀取,這樣可以節省大量的儲存(記憶體)空間。
舉個例子,比如,數學中有個著名的斐波拉契數列(Fibonacci),數列中第一個數為0,第二個數為1,其後的每一個數都可由前兩個數相加得到:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
現在我們想要通過for...in...迴圈來遍歷迭代斐波那契數列中的前n個數。那麼這個斐波那契數列我們就可以用迭代器來實現,每次迭代都通過數學計算來生成下一個數。
迭代器完成斐波那契的好處:
節省記憶體空間,因為每次根據演算法只生成一個值
生成數列的個數沒有上限控制
class Fibonacci(object):
def __init__(self, num):
# num:表示生成多少fibonacci數字
self.num = num
# 記錄fibonacci前兩個值
self.a = 0
self.b = 1
# 記錄當前生成數字的索引
self.current_index = 0
def __iter__(self):
return self
def __next__(self):
if self.current_index < self.num:
result = self.a
self.a, self.b = self.b, self.a + self.b
self.current_index += 1
return result
else:
raise StopIteration
fib = Fibonacci(5)
# value = next(fib)
# print(value)
for value in fib:
print(value)
小結
迭代器的作用就是是記錄當前資料的位置以便獲取下一個位置的值
3.生成器
<1>生成器的概念
生成器是一類特殊的迭代器,它不需要再像上面的類一樣寫__iter__()和__next__()方法了, 使用更加方便,它依然可以使用next函式和for迴圈取值
<2>建立生成器方法1
第一種方法很簡單,只要把一個列表推導式的 [ ] 改成 ( )
my_list = [i * 2 for i in range(5)]
print(my_list)
# 建立生成器
my_generator = (i * 2 for i in range(5))
print(my_generator)
# next獲取生成器下一個值
# value = next(my_generator)
# print(value)
for value in my_generator:
print(value)
<3>建立生成器方法2
在def函式裡面看到有yield關鍵字那麼就是生成器
def fibonacci(num):
a = 0
b = 1
# 記錄生成fibonacci數字的下標
current_index = 0
print("--11---")
while current_index < num:
result = a
a, b = b, a + b
current_index += 1
print("--22---")
# 程式碼執行到yield會暫停,然後把結果返回出去,下次啟動生成器會在暫停的位置繼續往下執行
yield result
print("--33---")
fib = fibonacci(5)
value = next(fib)
print(value)
value = next(fib)
print(value)
value = next(fib)
print(value)
# for value in fib:
# print(value)
在使用生成器實現的方式中,我們將原本在迭代器__next__方法中實現的基本邏輯放到一個函式中來實現,但是將每次迭代返回數值的return換成了yield,此時新定義的函式便不再是函式,而是一個生成器了。
簡單來說:只要在def中有yield關鍵字的 就稱為 生成器
<4>生成器使用return關鍵字
def fibonacci(num):
a = 0
b = 1
# 記錄生成fibonacci數字的下標
current_index = 0
print("--11---")
while current_index < num:
result = a
a, b = b, a + b
current_index += 1
print("--22---")
# 程式碼執行到yield會暫停,然後把結果返回出去,下次啟動生成器會在暫停的位置繼續往下執行
yield result
print("--33---")
return "嘻嘻"
fib = fibonacci(5)
value = next(fib)
print(value)
# 提示: 生成器裡面使用return關鍵字語法上沒有問題,但是程式碼執行到return語句會停止迭代,丟擲停止迭代異常
# 在python3裡面可以使用return關鍵字,python2不支援
# return 和 yield的區別
# yield: 每次啟動生成器都會返回一個值,多次啟動可以返回多個值,也就是yield可以返回多個值
# return: 只能返回一次值,程式碼執行到return語句就停止迭代
try:
value = next(fib)
print(value)
except StopIteration as e:
# 獲取return的返回值
print(e.value)
提示:
生成器裡面使用return關鍵字語法上沒有問題,但是程式碼執行到return語句會停止迭代,丟擲停止迭代異常
在python3裡面可以使用return關鍵字,python2不支援
<5>yield和return的對比
使用了yield關鍵字的函式不再是函式,而是生成器。(使用了yield的函式就是生成器)
程式碼執行到yield會暫停,然後把結果返回出去,下次啟動生成器會在暫停的位置繼續往下執行
每次啟動生成器都會返回一個值,多次啟動可以返回多個值,也就是yield可以返回多個值
return只能返回一次值,程式碼執行到return語句就停止迭代,丟擲停止迭代異常
<6>使用send方法啟動生成器並傳參
send方法啟動生成器的時候可以傳引數
def gen():
i = 0
while i<5:
temp = yield i
print(temp)
i+=1
next 和 send的區別:
next函式啟動生成器不能傳入引數
send方法啟動生成器可以傳入引數,但是第一次只能傳入None
注意:
如果第一次啟動生成器使用send方法,那麼引數只能傳入None,一般第一次啟動生成器使用next函式
小結
生成器建立有兩種方式,一般都使用yield關鍵字方法建立生成器
yield特點是程式碼執行到yield會暫停,把結果返回出去,再次啟動生成器在暫停的位置繼續往下執行
4.協程
<1>協程的概念
協程,又稱微執行緒,纖程,也稱為使用者級執行緒,在不開闢執行緒的基礎上完成多工,也就是在單執行緒的情況下完成多工,多個任務按照一定順序交替執行 通俗理解只要在def裡面只看到一個yield關鍵字表示就是協程
協程是也是實現多工的一種方式
協程yield的程式碼實現
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()
小結:
協程之間執行任務按照一定順序交替執行
5.greenlet
<1>greentlet的介紹
為了更好使用協程來完成多工,python中的greenlet模組對其封裝,從而使得切換任務變的更加簡單
使用如下命令安裝greenlet模組:
pip3 install greenlet
import time
import greenlet
# 任務1
def work1():
for i in range(5):
print("work1...")
time.sleep(0.2)
# 切換到協程2裡面執行對應的任務
g2.switch()
# 任務2
def work2():
for i in range(5):
print("work2...")
time.sleep(0.2)
# 切換到第一個協程執行對應的任務
g1.switch()
if __name__ == '__main__':
# 建立協程指定對應的任務
g1 = greenlet.greenlet(work1)
g2 = greenlet.greenlet(work2)
# 切換到第一個協程執行對應的任務
g1.switch()
6.gevent
<1>gevent的介紹
greenlet已經實現了協程,但是這個還要人工切換,這裡介紹一個比greenlet更強大而且能夠自動切換任務的第三方庫,那就是gevent。
gevent內部封裝的greenlet,其原理是當一個greenlet遇到IO(指的是input output 輸入輸出,比如網路、檔案操作等)操作時,比如訪問網路,就自動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行。
由於IO操作非常耗時,經常使程式處於等待狀態,有了gevent為我們自動切換協程,就保證總有greenlet在執行,而不是等待IO
安裝
pip3 install gevent
<2>gevent的使用
import gevent
def work(n):
for i in range(n):
# 獲取當前協程gevent.getcurrent()
print(gevent.getcurrent(), i)
g1 = gevent.spawn(work, 5)
g2 = gevent.spawn(work, 5)
g3 = gevent.spawn(work, 5)
g1.join()
g2.join()
g3.join()
3個greenlet是依次執行而不是交替執行
<3>gevent切換執行
import gevent
def work(n):
for i in range(n):
# 獲取當前協程
print(gevent.getcurrent(), i)
#用來模擬一個耗時操作,注意不是time模組中的sleep
gevent.sleep(1)
g1 = gevent.spawn(work, 5)
g2 = gevent.spawn(work, 5)
g3 = gevent.spawn(work, 5)
g1.join()
g2.join()
g3.join()
<4>給程式打補丁
gevent預設不會認為系統的耗時操作是耗時的,不識別time.sleep,accept,recv等,需要打補丁
import gevent
import time
from gevent import monkey
# 打補丁,讓gevent框架識別耗時操作,比如:time.sleep,accept,recv,網路請求延時等
monkey.patch_all()
# 任務1
def work1(num):
for i in range(num):
print("work1....")
time.sleep(0.2)
# gevent.sleep(0.2)
# 任務1
def work2(num):
for i in range(num):
print("work2....")
time.sleep(0.2)
# gevent.sleep(0.2)
if __name__ == '__main__':
# 建立協程指定對應的任務
g1 = gevent.spawn(work1, 3)
g2 = gevent.spawn(work2, 3)
# 主執行緒等待協程執行完成以後程式再退出
g1.join()
g2.join()
<5>注意
當前程式是一個死迴圈並且還能有耗時操作,就不需要加上join方法了,因為程式需要一直執行不會退出
示例程式碼
import gevent
import time
from gevent import monkey
# 打補丁,讓gevent框架識別耗時操作,比如:time.sleep,網路請求延時
monkey.patch_all()
# 任務1
def work1(num):
for i in range(num):
print("work1....")
time.sleep(0.2)
# gevent.sleep(0.2)
# 任務1
def work2(num):
for i in range(num):
print("work2....")
time.sleep(0.2)
# gevent.sleep(0.2)
if __name__ == '__main__':
# 建立協程指定對應的任務
g1 = gevent.spawn(work1, 3)
g2 = gevent.spawn(work2, 3)
while True:
print("主執行緒中執行")
time.sleep(0.5)
7.程序、執行緒、協程對比
<1>程序、執行緒、協程之間的關係
一個程序至少有一個執行緒,程序裡面可以有多個執行緒
一個執行緒裡面可以有多個協程
<2>程序、執行緒、執行緒的對比
程序是系統資源分配的基本單位
執行緒是作業系統排程的基本單位
程序切換需要的資源最大,效率很低
執行緒切換需要的資源一般,效率一般(當然了在不考慮GIL的情況下)
協程切換任務資源很小,效率高
多程序、多執行緒根據cpu核數不一樣可能是並行的,但是協程是在一個執行緒中 所以是併發
小結
程序、執行緒、協程都是可以完成多工的,可以根據自己實際開發的需要選擇使用
由於執行緒、協程需要的資源很少,所以使用執行緒和協程的機率最大
開闢協程需要的資源最少
協程和執行緒的區別是:協程避免了無意義的排程,由此可以提高效能,但也因此,程式設計師必須自己承擔排程的責任,同時,協程也失去了標準執行緒使用多CPU的能力。
程序擁有自己獨立的堆和棧,既不共享堆,亦不共享棧,程序由作業系統排程。
執行緒擁有自己獨立的棧和共享的堆,共享堆,不共享棧,執行緒亦由作業系統排程(標準執行緒是的)
8.gevent多工圖片下載
<1>多工圖片下載的示例程式碼
import gevent
import urllib.request # 網路請求模組
from gevent import monkey
# 打補丁: 讓gevent使用網路請求的耗時操作,讓協程自動切換執行對應的下載任務
monkey.patch_all()
# 根據圖片地址下載對應的圖片
def download_img(img_url, img_name):
try:
print(img_url)
# 根據圖片地址開啟網路資源資料
response = urllib.request.urlopen(img_url)
# 建立檔案把資料寫入到指定檔案裡面
with open(img_name, "wb") as img_file:
while True:
# 讀取網路圖片資料
img_data = response.read(1024)
if img_data:
# 把資料寫入到指定檔案裡面
img_file.write(img_data)
else:
break
except Exception as e:
print("圖片下載異常:", e)
else:
print("圖片下載成功: %s" % img_name)
if __name__ == '__main__':
# 準備圖片地址
img_url1 = "https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=551346117,2593226454&fm=27&gp=0.jpg"
img_url2 = "https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=829730016,3409799239&fm=27&gp=0.jpg"
img_url3 = "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1815077192,817368579&fm=27&gp=0.jpg"
# 建立協程指派對應的任務
g1 = gevent.spawn(download_img, img_url1, "1.jpg")
g2 = gevent.spawn(download_img, img_url2, "2.jpg")
g3 = gevent.spawn(download_img, img_url3, "3.jpg")
# 主執行緒等待所有的協程執行完成以後程式再退出
gevent.joinall([g1, g2, g3])
依次根據圖片地址去下載,但是收到資料的先後順序不一定與傳送順序相同,這也就體現出了非同步,即不確定什麼時候會收到資料,順序不一定