文章目錄
- 迭代器
- 可迭代對象
- 可迭代對象的本質
- iter()函數與 next()函數
- 迭代器 Iterator
- 樣例
- for...in...循環的本質
- 使用的場景--斐波那契數列
- list和tuple也可以接收可迭代對象
- 生成器
- 簡介
- 創建生成器
- 方法一
- 方法二
- 總結
- 使用 send 喚醒
- 協程
- 協程和線程差異
- 簡單實現協程
- greenlet
- gevent
- 安裝
- gevent 的使用方法
- 給程序打補丁
- gevent的常用方法
- python最新接口
- 最后的示例
- 并行下載
迭代器
迭代是訪問集合元素的一種方式。迭代器是一個可以記住遍歷的位置的對象。迭代器對象從集合的第一個元素開始訪問,直到所有的元素被訪問完結束。迭代器只能往前不會后退。
可迭代對象
我們已經知道可以對list、tuple、str 等類型的數據使用 for…in…的循環語法從其中依次拿到數據進行使用,我們把這樣的過程稱為遍歷,也叫迭代。
我們把可以通過 for…in…這類語句迭代讀取一條數據供我們使用的對象稱之為可迭代對象(Iterable)。
可以使用 isinstance() 判斷該對象是否是一個可迭代的對象
In [50]: from collections.abc import Iterable
In [51]: isinstance([], Iterable)
Out[51]: True
In [52]: isinstance({}, Iterable)
Out[52]: True
In [53]: isinstance('abc', Iterable)
Out[53]: True
In [54]: isinstance(mylist, Iterable)
Out[54]: False
In [55]: isinstance(100, Iterable)
Out[55]: False
可迭代對象的本質
可迭代對象通過__iter__方法向我們提供一個迭代器,我們在迭代一個可迭代對
象的時候,實際上就是先獲取該對象提供的一個迭代器,然后通過這個迭代器來依次獲取對象中的每一個數據. 那么也就是說,一個具備了__iter__方法的對象,就是一個可迭代對象,所以關鍵就在于我們要如何去重寫這一個__iter__方法。
iter()函數與 next()函數
list、tuple 等都是可迭代對象,我們可以通過 iter()函數獲取這些可迭代對象的迭代器。然后我們可以對獲取到的迭代器不斷使用 next()函數來獲取下一條數據。
iter()函數實際上就是調用了可迭代對象的__iter__方法
注意,當我們已經迭代完最后一個數據之后,再次調用 next()函數會拋出StopIteration 的異常,來告訴我們所有數據都已迭代完成,不用再執行 next()函數了
迭代器 Iterator
通過上面的分析,我們已經知道,迭代器是用來幫助我們記錄每次迭代訪問到的位置,當我們對迭代器使用 next()函數的時候,迭代器會向我們返回它所記錄位置的下一個位置的數據。實際上,在使用 next()函數的時候,調用的就是迭代器對象的__next__方法(Python3 中是對象的__next__方法)。所以,我們要想構造一個迭代器,就要實現它的__next__方法。但這還不夠,python 要求迭代器本身也是可迭代的,所以我們還要為迭代器實現__iter__方法,而__iter__方法要返回一個迭代器,迭代器自身正是一個迭代器,所以迭代器的__iter__方法返回自身即可。
一個實現了__iter__方法和__next__方法的對象,就是迭代器。
樣例
from collections.abc import Iterable
# 自定義可迭代對象
class MyList:def __init__(self):self.mylist = []def add(self,num):self.mylist.append(num)# 此處是關鍵,定義了這一個之后,就相當于是讓這個對象是可迭代對象def __iter__(self):return MyIterator(self)# 自定義迭代器
class MyIterator:def __init__(self,mylist):self.mylist:MyList = mylistself.current = 0def __iter__(self):return selfdef __next__(self):if self.current > len(self.mylist.mylist) - 1:# 此處一定要實現拋出StopIteration異常raise StopIterationelse:current = self.currentself.current = self.current + 1return self.mylist.mylist[current]if __name__ == '__main__':mylist = MyList()mylist.add(1)mylist.add(2)mylist.add(3)for i in mylist:print(i)print(isinstance(mylist,Iterable))
for…in…循環的本質
for item in Iterable 循環的本質就是先通過 iter()函數獲取可迭代對象 Iterable 的迭代器,然后對獲取到的迭代器不斷調用 next()方法來獲取下一個值并將其賦值給item,當遇到 StopIteration 的異常后循環
使用的場景–斐波那契數列
class FibIterator(object):"""斐波那契數列迭代器"""def __init__(self, n):""":param n: int, 指明生成數列的前 n 個數"""self.n = n# current 用來保存當前生成到數列中的第幾個數了self.current = 0# num1 用來保存前前一個數,初始值為數列中的第一個數 0self.num1 = 0# num2 用來保存前一個數,初始值為數列中的第二個數 1self.num2 = 1def __next__(self):"""被 next()函數調用來獲取下一個數"""if self.current < self.n:num = self.num1self.num1, self.num2 = self.num2, self.num1+self.num2self.current += 1return numelse:raise StopIterationdef __iter__(self):"""迭代器的__iter__返回自身即可"""return selfif __name__ == '__main__':fib = FibIterator(10)for num in fib:print(num, end=" ")
list和tuple也可以接收可迭代對象
除了 for 循環能接收可迭代對象,list、tuple 等也能接收
li = list(FibIterator(15))
print(li)
tp = tuple(FibIterator(6))
print(tp)
生成器
簡介
利用迭代器,我們可以在每次迭代獲取數據(通過 next()方法)時按照特定的規律進行生成。但是我們在實現一個迭代器時,關于當前迭代到的狀態需要我們自己記錄,進而才能根據當前狀態生成下一個數據。為了達到記錄當前狀態,并配合next()函數進行迭代使用,我們可以采用更簡便的語法,即生成器(generator)。生成器是一類特殊的迭代器。
創建生成器
方法一
實際上就是把一個列表生成式的 [ ] 改成 ( )
my_generator = ( x*2 for x in range(5))
print(my_generator)
print(next(my_generator))
print(next(my_generator))
print(next(my_generator))
print(next(my_generator))
print(next(my_generator))
對于生成器來說,我們可以按照迭代器的使用方法來使用,即可以通過 next()函數、for 循環、list()等方法進行迭代
方法二
generator 非常強大。如果推算的算法比較復雜,用類似列表生成式的 for 循環無法實現的時候,還可以用函數來實現。
在使用生成器實現的方式中,我們將原本在迭代器__next__方法中實現的基本邏輯放到一個函數中來實現,但是將每次迭代返回數值的 return 換成了 yield,此時新定義的函數便不再是函數,而是一個生成器了。簡單來說:只要在 def 中有yield 關鍵字的 就稱為 生成器此時按照調用函數的方式( 案例中為 F = fib(5) )使用生成器就不再是執行函數體了,而是會返回一個生成器對象( 案例中為 F ),然后就可以按照使用迭代器的方式來使用生成器了。
# 含有yield的函數稱為生成器
def fib(n):current = 0num1, num2 = 0, 1while current < n:num = num1num1, num2 = num2, num1 + num2current += 1# print(num,end=' ')yield numreturn 'done'# F是一個生成器,支持next
F=fib(10)print(F)# for i in F:
# print(i,end=' ')# 迭代生成
l=[ i for i in F ]
print(l)# 想要拿取return當中的值
try:next(F)
except StopIteration as e:print("\n返回值為:{}".format(e.value))
用 for 循環調用 generator 時,發現拿不到 generator 的 return 語句的返回
值。如果想要拿到返回值,必須捕獲 StopIteration 錯誤,返回值包含在
StopIteration 的 value 中
這里還有一個小細節,讀者可能回想為什么我們在使用已經做好的list時候對于list進行for in處理,可以多次處理多次拿值,但是對于我們上文寫的迭代器為什么不行?原因就在于我們實際上list叫做可迭代對象,它實際上所采用的是我們最上面的那種手法,定義兩個類的手法,每次迭代,都會有一個新的迭代器來運行。
總結
- 使用了 yield 關鍵字的函數不再是函數,而是生成器。(使用了 yield 的函數
就是生成器) - yield 關鍵字有兩點作用:
– 保存當前運行狀態(斷點),然后暫停執行,即將生成器(函數)掛起
– 將 yield 關鍵字后面表達式的值作為返回值返回,此時可以理解為起到了 return 的作用 - 可以使用 next()函數讓生成器從斷點處繼續執行,即喚醒生成器(函數)
- Python3 中的生成器可以使用 return 返回最終運行的返回值
使用 send 喚醒
我們除了可以使用 next()函數來喚醒生成器繼續執行外,還可以使用 send()函數來喚醒執行。使用 send()函數的一個好處是可以在喚醒的同時向斷點處傳入一個附加數據。
def gen():i=0while i<5:temp=yield iprint(temp)i+=1g=gen()next(g)
g.send('hello')
協程
協程,又稱微線程,纖程。英文名 Coroutine。
協程是 python 個中另外一種實現多任務的方式,只不過比線程更小占用更小執行單元(理解為需要的資源)。 為啥說它是一個執行單元,因為它自帶 CPU 上下文。這樣只要在合適的時機, 我們可以把一個協程 切換到另一個協程。 只要這個過程中保存或恢復 CPU 上下文那么程序還是可以運行的。
通俗的理解:在一個線程中的某個函數,可以在任何地方保存當前函數的一些臨時變量等信息,然后切換到另外一個函數中執行,注意不是通過調用函數的方式做到的,并且切換的次數以及什么時候再切換到原來的函數都由開發者自己確定。
協程和線程差異
在實現多任務時, 線程切換從系統層面遠不止保存和恢復 CPU 上下文這么簡單。操作系統為了程序運行的高效性每個線程都有自己緩存 Cache 等等數據,操作系統還會幫你做這些數據的恢復操作。 所以線程的切換非常耗性能。但是協程的切換只是單純的操作 CPU 的上下文,所以一秒鐘切換個上百萬次系統都抗的住。
簡單實現協程
import timedef work1():while True:print("----work1---")yieldtime.sleep(0.5)def work2():while True:print("----work2---")yieldtime.sleep(0.5)def main():w1 = work1()w2 = work2()while True:next(w1)next(w2)if __name__ == "__main__":main()
greenlet
基本的使用方法:
from greenlet import greenlet
import timedef test1():while True:print("---A--")gr2.switch()time.sleep(0.5)def test2():while True:print("---B--")gr1.switch()time.sleep(0.5)gr1 = greenlet(test1)
gr2 = greenlet(test2)
#切換到 gr1 中運行
gr1.switch()
gevent
greenlet 已經實現了協程,但是這個還的人工切換,是不是覺得太麻煩了,python 還有一個比 greenlet 更強大的并且能夠自動切換任務的模塊 gevent,其原理是當一個 greenlet 遇到 IO(指的是 input output 輸入輸出,比如網絡、文件操作等)操作時,比如訪問網絡,就自動切換到其他的 greenlet,等到 IO 操作完成,再在適當的時候切換回來繼續執行。
由于 IO 操作非常耗時,經常使程序處于等待狀態,有了 gevent 為我們自動切換協程,就保證總有協程在運行,而不是等待 IO時間
參考鏈接
安裝
pip install gevent -i https://pypi.tuna.tsinghua.edu.cn/simple
gevent 的使用方法
gevent.spawn 接口使用方法gevent.spawn(函數名,傳參)
import geventdef f(n):for i in range(n):print(gevent.getcurrent(), i)#用來模擬一個耗時操作,注意不是 time 模塊中的 sleepgevent.sleep(1)g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()
運行結果:
<Greenlet at 0x7fa70ffa1c30: f(5)> 0
<Greenlet at 0x7fa70ffa1870: f(5)> 0
<Greenlet at 0x7fa70ffa1eb0: f(5)> 0
<Greenlet at 0x7fa70ffa1c30: f(5)> 1
<Greenlet at 0x7fa70ffa1870: f(5)> 1
<Greenlet at 0x7fa70ffa1eb0: f(5)> 1
<Greenlet at 0x7fa70ffa1c30: f(5)> 2
<Greenlet at 0x7fa70ffa1870: f(5)> 2
<Greenlet at 0x7fa70ffa1eb0: f(5)> 2
<Greenlet at 0x7fa70ffa1c30: f(5)> 3
<Greenlet at 0x7fa70ffa1870: f(5)> 3
<Greenlet at 0x7fa70ffa1eb0: f(5)> 3
<Greenlet at 0x7fa70ffa1c30: f(5)> 4
<Greenlet at 0x7fa70ffa1870: f(5)> 4
<Greenlet at 0x7fa70ffa1eb0: f(5)> 4
給程序打補丁
猴子補丁作用:
monkey patch 指的是在執行時動態替換,通常是在 startup 的時候. 用過 gevent 就會知道,會在最開頭的地方 gevent.monkey.patch_all();把標準庫中的 thread/socket 等給替換掉.這樣我們在后面使用 socket的時候能夠跟尋常一樣使用,無需改動不論什么代碼,可是它變成非堵塞的了
from gevent import monkey
import gevent
import random
import time# 這句話是關鍵
monkey.patch_all()def coroutine_work(coroutine_name):for i in range(10):print(coroutine_name, i)time.sleep(random.random())gevent.joinall([gevent.spawn(coroutine_work, "work1"),gevent.spawn(coroutine_work, "work2")
])
gevent的常用方法
常用方法 | 說明 |
---|---|
gevent.spawn() | 創建一個普通的 Greenlet 對象并切換 |
gevent.spawn_later(seconds=3) | 延時創建一個普通的 Greenlet 對象并切換 |
gevent.spawn_raw() | 創建的協程對象屬于一個組 |
gevent.getcurrent() | 返回當前正在執行的 greenlet |
gevent.joinall(jobs) | 將協程任務添加到事件循環,接收一個任務列表 |
gevent.wait() | 可以替代 join 函數等待循環結束,也可以傳入協程對象列表 |
gevent.kill() | 殺死一個協程 |
gevent.killall() | 殺死一個協程列表里的所有協程 |
monkey.patch_all() | 非常重要,會自動將 python 的一些標準模塊替換成 gevent框架 |
python最新接口
鏈接
官方文檔:
鏈接
鏈接
最后的示例
并行下載
from gevent import monkey
import gevent
import urllib.request# 有耗時操作時需要
monkey.patch_all()def my_downLoad(url):print('GET: %s' % url)resp = urllib.request.urlopen(url)data = resp.read()print('%d bytes received from %s.' % (len(data), url))gevent.joinall([gevent.spawn(my_downLoad, 'http://www.baidu.com/'),gevent.spawn(my_downLoad, 'http://www.cskaoyan.com/'),gevent.spawn(my_downLoad, 'http://www.qq.com/'),
])
以及
from gevent import monkey
import gevent
import urllib.request#有 IO 才做時需要這一句
monkey.patch_all()def my_downLoad(file_name, url):print('GET: %s' % url)resp = urllib.request.urlopen(url)data = resp.read()with open(file_name, "wb") as f:f.write(data)print('%d bytes received from %s.' % (len(data), url))gevent.joinall([gevent.spawn(my_downLoad,"7a082c0dde36eac2205a088397aaf295.jpg",'http://qzs.qq.com/qzone/v6/v6_config/upload/7a082c0dde36eac2205a088397aaf295.jpg'),gevent.spawn(my_downLoad,"da8e974dc_is.jpg",'https://pic1.zhimg.com/da8e974dc_is.jpg'),])
上面的 url 可以換為自己需要下載視頻、音樂、圖片等url