Python 生成器 和 yield 关键字
?
Python 中 yield 的作用:http://youchen.me/2017/02/10/Python-What-does-yield-do/#
Python 生成器詳解:http://codingpy.com/article/python-generator-notes-by-kissg/#generator
Python yield與實現(xiàn):http://www.cnblogs.com/coder2012/p/4990834.html
?
?
1. 迭代(iteration)、迭代器(iterator)、generator(生成器)
?
1.1 迭代
?
為了搞清楚 yield 是用來做什么的,首先得知道 Python 中 generator(生成器) 的相關概念,要理解 generator(生成器) ,的先從 迭代(iteration) 與 迭代器(iterator) 講起。
迭代是重復反饋過程的活動,其目的通常是為了接近并到達所需的目標或結果。每一次對過程的重復被稱為一次“迭代”,而每一次迭代得到的結果會被用來作為下一次迭代的初始值。————? 以上是?維基百科?對迭代的定義。
在 Python中,迭代 通常是通過?for ... in ...來完成的,而且只要是?可迭代對象(iterable),都能進行迭代。
當你創(chuàng)建一個了列表,你可以逐個遍歷列表中的元素,而這個過程便叫做?迭代 。
>>> mylist = [1, 2, 3] >>> for i in mylist: ... print(i) 1 2 3而?mylist是一個可迭代對象。當你使用列表推導式的時候,創(chuàng)建了一個列表,他也是可迭代對象:
>>> mylist = [x*x for x in range(3)] >>> for i in mylist: ... print(i) 0 1 4所有能夠接受?for...in...操作的對象都是 可迭代對象,如:列表、字符串、文件等。這些可迭代對象用起來都十分順手。?因為你可以按照你的想法去訪問它們,但是你把所有數(shù)據(jù)都保存在了內(nèi)存中,而當你有大量數(shù)據(jù)的時候這可能并不是你想要的結果。
?
1.2 可迭代對象(iterable) 與 迭代器(iterator)
?
這里簡單講下??可迭代對象(iterable)與 迭代器(iterator):
- 1.可迭代對象(iterable)?是實現(xiàn)了__iter__()方法的對象。更確切的說,是container.__iter__()方法,該方法返回的是的一個iterator對象,因此iterable是你可以從其獲得iterator的對象。對于iterable,重點關注的是:它是一個能一次返回一個成員的對象(iterable is an object capable of returning its members one at a time),一些?iterable?將所有值都存儲在內(nèi)存中,比如list,而另一些并不是這樣,比如我們下面將講到的?iterator.
- 2. 迭代器(iterator)?是實現(xiàn)了iterator.__iter__()?和?iterator.__next__()方法的對象。iterator.__iter__()方法返回的是iterator對象本身。根據(jù)官方的說法,正是這個方法,實現(xiàn)了for ... in ...語句。而?iterator.__next__()是??iterator?區(qū)別于?iterable?的關鍵,它允許我們?顯式?地獲取一個元素。當調(diào)用?next()方法時,實際上產(chǎn)生了2個操作:?
? ? ? ? 1. 更新 iterator 狀態(tài),令其指向后一項,以便下一次調(diào)用
? ? ? ? 2. 返回當前結果
如果你學過?C++,它其實跟指針的概念很像(如果你還學過鏈表的話,或許能更好地理解)。
正是?__next__(),使得iterator能在每次被調(diào)用時,返回一個單一的值(有些教程里,稱為一邊循環(huán),一邊計算,我覺得這個說法不是太準確。但如果這樣的說法有助于你的理解,我建議你就這樣記),從而極大的節(jié)省了內(nèi)存資源。另一點需要格外注意的是,iterator是消耗型的,即每一個值被使用過后,就消失了。因此,你可以將以上的操作2理解成pop。對iterator進行遍歷之后,其就變成了一個空的容器了,但不等于None哦。因此,若要重復使用iterator,利用list()方法將其結果保存起來是一個不錯的選擇。
我們通過代碼來感受一下。
>>> from collections import Iterable, Iterator >>> a = [1,2,3] # 眾所周知,list是一個iterable >>> b = iter(a) # 通過iter()方法,得到iterator,iter()實際上調(diào)用了__iter__(),此后不再多說 >>> isinstance(a, Iterable) True >>> isinstance(a, Iterator) False >>> isinstance(b, Iterable) True >>> isinstance(b, Iterator) True # 可見,iterable是iterator,但iterator不一定是iterable?iterator 是消耗型的,用一次少一次。對 iterator 進行遍歷,iterator就空了!
# iterator是消耗型的,用一次少一次.對iterator進行遍歷,iterator就空了! >>> c = list(b) >>> c [1, 2, 3]# c 已經(jīng)遍歷過了,所以 d 就為 空 了 >>> d = list(b) >>> d []空的 iterator 并不等于 None。
>>> if b: ... print(1) ... 1 >>> if b == None: ... print(1) ...再來感受一下 next()
>>> e = iter(a) >>> next(e) #next()實際調(diào)用了__next__()方法,此后不再多說 1 >>> next(e) 2既然提到了for ... in ...語句,我們再來簡單講下其工作原理吧,或許能幫助理解以上所講的內(nèi)容。
>>> x = [1, 2, 3] >>> for i in x: ... ...我們對一個iterable用for ... in ...進行迭代時,實際是先通過調(diào)用iter()方法得到一個iterator,假設叫做X。然后循環(huán)地調(diào)用X的next()方法取得每一次的值,直到iterator為空,返回的StopIteration作為循環(huán)結束的標志。for ... in ...會自動處理StopIteration異常,從而避免了拋出異常而使程序中斷。如圖所示
磨刀不誤砍柴工,有了前面的知識,我們再來理解?generator?與?yield?將會事半功倍。
?
?
2. 生成器
?
生成器 是 這樣一個函數(shù),它記住上一次返回時在函數(shù)體中的位置。
對生成器函數(shù)的第二次(或第 n 次)調(diào)用跳轉(zhuǎn)至該函數(shù)中間,而上次調(diào)用的所有局部變量都保持不變。
生成器不僅 “記住” 了它的數(shù)據(jù)狀態(tài);生成器還 “記住” 了它的流控制構造(在命令式編程中,這種構造不只是數(shù)據(jù)值)中的位置。?
生成器的特點:
- 1. 生成器是一個函數(shù),而且函數(shù)的參數(shù)都會保留。
- 2. 迭代到下一次的調(diào)用時,所使用的參數(shù)都是第一次所保留下的,即是說,在整個所有函數(shù)調(diào)用的參數(shù)都是第一次所調(diào)用時保留的,而不是新創(chuàng)建的
- 3. 節(jié)約內(nèi)存
- 一個生成器函數(shù)的定義很像一個普通的函數(shù),除了當它要生成一個值的時候,使用 yield 關鍵字而不是 return。如果一個 def 的主體包含 yield,這個函數(shù)會自動變成一個生成器(即使它包含一個 return)。創(chuàng)建一個生成器就這么簡單。。。
生成器 比 迭代器 更加強大也更加復雜,需要花點功夫好好理解貫通。先理清幾個概念:
generator: A function which returns a generator iterator. It looks like a normal function except that it contains yield expressions for producing a series of values usable in a for-loop or that can be retrieved one at a time with the next() function. generator iterator: An object created by a generator funcion. generator expression: An expression that returns an iterator.
以上的定義均來自?python官方文檔。可見,我們常說的生成器,就是帶有?yield?的函數(shù),而?generator iterator?則是generator function?的返回值,即一個generator對象,而形如(elem for elem in [1, 2, 3])的表達式,稱為generator expression,實際使用與generator無異。
生成器 也是 迭代器,但是你只能對它們進行一次迭代,原因在于它們并沒有將所有數(shù)據(jù)存儲在內(nèi)存中,而是即時生成這些數(shù)據(jù):
>>> mygenerator = (x*x for x in range(3)) >>> for i in mygenerator: ... print(i) 0 1 4這一段代碼和上面 迭代?那段很相似,唯一不同的地方是使用了()代替?[]。但是,這樣的后果是你無法對?mygenerator進行第二次?for i in mygenerator,因為生成器只能被使用一次:它首先計算出結果0,然后忘記它再計算出1,最后是4,一個接一個。
>>> a = (elem for elem in [1, 2, 3]) >>> a <generator object <genexpr> at 0x7f0d23888048> >>> def fib(): ... a, b = 0, 1 ... while True: ... yield b ... a, b = b, a + b ... >>> fib <function fib at 0x7f0d238796a8> >>> b = fib() <generator object fib at 0x7f0d20bbfea0>其實說白了,generator?就是?iterator?的一種,以更優(yōu)雅的方式實現(xiàn)的?iterator。
官方的說法是:Python’s generators provide a convenient way to implement the iterator protocol.
你完全可以像使用?iterator?一樣使用?generator,但是請記住他們兩個的定義不一樣:
- 定義一個?iterator,你需要分別實現(xiàn)?__iter__()?方法和?__next__()?方法,
- 但?generator?只需要一個小小的?yield?( 好吧,generator expression?的使用比較簡單,就不展開講了。)
?
?
3. yield 關鍵字
?
Python 中 生成器 是使用? yield 關鍵字 來實現(xiàn)的。
- 1. yield?是一個用法跟?return?很相似的關鍵字,但是使用yield的函數(shù)返回的是一個 生成器。
- 2. yield 可以暫停一個函數(shù)并返回中間結果。使用 yield 的函數(shù) 將 保存執(zhí)行環(huán)境,即函數(shù)的參數(shù)都會保留,并且在必要時恢復。到下一次的調(diào)用時,所有參數(shù)都恢復,所使用的參數(shù)都是第一次所保留下的參數(shù)和環(huán)境,然后從先前暫停的地方開始執(zhí)行,直到遇到下一個 yield 再次 暫停。
看一段代碼:
def test_func():for i in range(5):yield i #print(i + 100)t = test_func() for i in t:print(i)pass可以 單步調(diào)試 上面這個代碼,就可以 驗證 上面 兩個 特點。
前文講到?iterator?通過?__next__()方法實現(xiàn)了每次調(diào)用,返回一個單一值的功能。而?yield?就是實現(xiàn)?generator?的?__next__()方法的關鍵!先來看一個最簡單的例子:
>>> def g(): ... print("1 is") ... yield 1 ... print("2 is") ... yield 2 ... print("3 is") ... yield 3 ... >>> z = g() >>> z <generator object g at 0x7f0d2387c8b8> >>> next(z) 1 is 1 >>> next(z) 2 is 2 >>> next(z) 3 is 3 >>> next(z) Traceback (most recent call last):File "<stdin>", line 1, in <module> StopIteration解釋:
- 第一次調(diào)用?next()方法時,函數(shù)似乎執(zhí)行到?yield 1,就暫停了。
? ? (執(zhí)行next方法,使程序運行到 yield 處暫停并返回1,然后交出控制權,然后 next 接收控制權,并獲得 yield 的返回值 1) - 第二次調(diào)用?next()方法時,函數(shù)從?yield 1之后開始執(zhí)行的,并再次暫停。
- 第三次調(diào)用?next()方法時,從第二次暫停的地方開始執(zhí)行。
- 第四次調(diào)用?next()方法時,拋出StopIteration?異常。
事實上,generator?確實在遇到?yield?之后暫停了,確切點說,是先返回了?yield?表達式的值,再暫停的。當再次調(diào)用?next()時,從先前暫停的地方開始執(zhí)行,直到遇到下一個?yield。這與上文介紹的對iterator調(diào)用next()方法,執(zhí)行原理一般無二。
有些教程里說?generator?保存的是算法,而我覺得用?中斷服務子程序?來描述?generator?或許能更好理解,這樣你就能將 yield?理解成一個中斷服務子程序的?斷點,沒錯,是中斷服務子程序的斷點。我們每次對一個?generator對象調(diào)用?next()時,函數(shù)內(nèi)部代碼執(zhí)行到 "斷點"?yield,然后返回這一部分的結果,并保存上下文環(huán)境,"中斷" 返回。
怎么樣,是不是瞬間就明白了yield?的用法
我們再來看另一段代碼。
>>> def gen(): ... while True: ... s = yield ... print(s) ... >>> g = gen() >>> g.send("kissg") Traceback (most recent call last):File "<stdin>", line 1, in <module> TypeError: can't send non-None value to a just-started generator >>> next(g) >>> g.send("kissg") kissg我正是看到這個形式的?generator,懵了,才想要深入學習?generator與?yield?的。結合以上的知識,我再告訴你,generator其實有 第 2 種調(diào)用方法(恢復執(zhí)行),即通過?send(value)?方法將?value?作為?yield?表達式的當前值,你可以用該值再對其他變量進行賦值,這一段代碼就很好理解了。當我們調(diào)用send(value)方法時,generator正由于yield的緣故被暫停了。此時,send(value)?方法傳入的值作為?yield?表達式的值,函數(shù)中又將該值賦給了變量?s,然后 print 函數(shù)打印?s,循環(huán)再遇到y(tǒng)ield,暫停返回。
調(diào)用?send(value)?時要注意,要確保?generator?是在?yield?處被暫停了,如此才能向?yield表達式傳值,否則將會報錯(如上所示),可通過?next()方法或?send(None)使?generator執(zhí)行到?yield。
?
再來看一段?yield?更復雜的用法,或許能加深你對?generator?的?next()與?send(value)的理解。
def echo(value=None):while 1:value = (yield value)print("The value is", value)if value:value += 1print('add +1 value', value)# 調(diào)用send(value)時要注意,要確保generator是在yield處被暫停了, # 如此才能向yield表達式傳值,否則將會報錯 # 可通過next()方法或send(None)使generator執(zhí)行到y(tǒng)ield。 # 生成器(generator) 有兩種方法 恢復執(zhí)行:1. send() 方法。2. next() 方法g = echo(1) # 返回一個 生成器 print(next(g)) # 通過 next() 方法 使 生成器 執(zhí)行到 yield 處暫停 g.send(2) # send(value)方法傳入的值作為yield表達式的值 g.send(5) next(g) next(g) next(g) """ 執(zhí)行結果: 1 The value is 2 add +1 value 3 The value is 5 add +1 value 6 The value is None The value is None The value is None """上述代碼既有?yield value?的形式,又有?value = yield?形式,看起來有點復雜。但以?yield?分離代碼進行解讀,就不太難了。
- 第一次調(diào)用?next()方法,執(zhí)行到?yield value表達式,保存上下文環(huán)境暫停返回?1。
- 第二次調(diào)用?send(value)方法,從?value = yield?開始,打印,再次遇到?yield value?暫停返回。
- 后續(xù)的調(diào)用?send(value) 或?next()都是如此。
但是,這里就引出了另一個問題,yield?作為一個暫停恢復的點,代碼從?yield?處恢復,又在下一個?yield?處暫停。可見,在一次?next()(非首次) 或?send(value)調(diào)用過程中,實際上存在?2?個?yield
- 一個作為恢復點的?yield
- 一個作為暫停點的yield
因此,也就有 2 個?yield 表達式。send(value)方法是將值傳給恢復點yield。調(diào)用next()表達式的值時,其恢復點yield的值總是為None,而將暫停點的yield表達式的值返回。為方便記憶,你可以將此處的恢復點記作當前的(current),而將暫停點記作下一次的(next),這樣就與next()方法匹配起來啦。
generator還實現(xiàn)了另外兩個方法throw(type[, value[, traceback]])與close()。前者用于拋出異常,后者用于關閉generator.不過這2個方法似乎很少被直接用到,本文就不再多說了,有興趣的同學請看這里。
?
示 例 解 釋
>>> def createGenerator(): ... mylist = range(3) ... for i in mylist: ... yield i*i ... >>> mygenerator = createGenerator() # create a generator >>> print(mygenerator) # mygenerator is an object! <generator object createGenerator at 0xb7555c34> >>> for i in mygenerator: ... print(i) 0 1 4這是一個沒有什么用的例子,但是用來讓你了解當你知道你的函數(shù)會返回一個只會被遍歷1次的巨大數(shù)據(jù)集合該怎么做的時候十分方便。為了掌握yield,你必須了解當你調(diào)用這個函數(shù)的時候,你在函數(shù)體中寫的代碼并沒有被執(zhí)行,而是只返回了一個生成器對象,這個需要特別注意。然后,你的代碼將會在每次for使用這個生成器的時候被執(zhí)行。最后,最困難的部分:
for第一次調(diào)用通過你函數(shù)創(chuàng)建的生成器對象的時候,它將會從你函數(shù)的開頭執(zhí)行代碼,一直到到達yield,然后它將會返回循環(huán)中的第一個值。然后,其他每次調(diào)用都會再一次執(zhí)行你在函數(shù)中寫的那段循環(huán),并返回下一個值,直到?jīng)]有值可以返回。
生成器在函數(shù)執(zhí)行了卻沒有到達yield的時候?qū)⒈徽J為是空的,原因在于循環(huán)到達了終點,或者不再滿足if/else條件。
>>> class Bank(): # let's create a bank, building ATMs ... crisis = False ... def create_atm(self): ... while not self.crisis: ... yield "$100" >>> hsbc = Bank() # when everything's ok the ATM gives you as much as you want >>> corner_street_atm = hsbc.create_atm() >>> print(corner_street_atm.next()) $100 >>> print(corner_street_atm.next()) $100 >>> print([corner_street_atm.next() for cash in range(5)]) ['$100', '$100', '$100', '$100', '$100'] >>> hsbc.crisis = True # crisis is coming, no more money! >>> print(corner_street_atm.next()) <type 'exceptions.StopIteration'> >>> wall_street_atm = hsbc.create_atm() # it's even true for new ATMs >>> print(wall_street_atm.next()) <type 'exceptions.StopIteration'> >>> hsbc.crisis = False # trouble is, even post-crisis the ATM remains empty >>> print(corner_street_atm.next()) <type 'exceptions.StopIteration'> >>> brand_new_atm = hsbc.create_atm() # build a new one to get back in business >>> for cash in brand_new_atm: ... print cash $100 $100 $100 $100 $100 $100 $100 $100 $100 ...首先看生成器的next方法,它用來執(zhí)行代碼并從生成器中獲取下一個元素(在Python 3.x中生成器已經(jīng)沒有next方法,而是使用next(iterator)代替)。在crisis未被置為True的時候,create_atm函數(shù)中的while循環(huán)可以看做是無盡的,當crisis為True的時候,跳出了while循環(huán),所有迭代器將會到達函數(shù)尾部,此時再次訪問next將會拋出StopIteration異常,而此時就算將crisis設置為False,這些生成器仍然處在函數(shù)尾部,訪問會繼續(xù)拋出StopIteration異常。
將以上例子用來控制訪問資源等用途的時候十分有用。
?
示例解析:
#generation.py def gen():for x in xrange(4):tmp = yield xif tmp == "hello":print "world"else:print "12345abcd", str(tmp)>>>from generation import gen >>>c=gen() >>>c.next() 0 >>>c.next() 12345abcd None 1 >>>c.send("python") 12345abcd python2執(zhí)行到 yield 時,gen 函數(shù)暫時停止并保存,返回x的值,同時tmp接收send的值(ps:yield x 相當于 return x ,所以第一次c.next()結果是0。第二次c.next()時,繼續(xù)在原來暫停的地方執(zhí)行,因為沒有send 值,所以tmp 為 None。c.next()等價c.send(None))。下次c.send(“python”),send發(fā)送過來的值,c.next()等價c.send(None)
了解了next()如何讓包含yield的函數(shù)執(zhí)行后,我們再來看另外一個非常重要的函數(shù)send(msg)。其實next()和send()在一定意義上作用是相似的,區(qū)別是send()可以傳遞yield表達式的值進去,而next()不能傳遞特定的值,只能傳遞None進去。因此,我們可以看做c.next() 和 c.send(None) 作用是一樣的。
需要提醒的是,第一次調(diào)用時,請使用next()語句或是send(None),不能使用send發(fā)送一個非None的值,否則會出錯的,因為沒有Python?yield語句來接收這個值。
?
示例解析:
def gen():for x in range(4):tmp = yield xif tmp == 'hello':print('world')else:print(str(tmp))只要函數(shù)中包含yield關鍵字,該函數(shù)調(diào)用就是生成器對象。
import typesdef gen():for x in range(4):tmp = yield xif tmp == 'hello':print('world')else:print(str(tmp))g = gen() print(g) # <generator object gen at 0x02801760> print(isinstance(g, types.GeneratorType)) # True我們可以看到,gen() 并不是函數(shù)調(diào)用,而是產(chǎn)生生成器對象。
生成器對象支持幾個方法,如 gen.next() ,gen.send() ,gen.throw() 等。
print(g.next()) # 0調(diào)用生成器的 next 方法,將運行到 yield 位置,此時暫停執(zhí)行環(huán)境,并返回yield后的值。所以打印出的是0,暫停執(zhí)行環(huán)境。
print(g.next())?# None??1再調(diào)用 next 方法,你也許會好奇,為啥打印出兩個值,不急,且聽我慢慢道來:上一次調(diào)用 next,執(zhí)行到 yield 0暫停,再次執(zhí)行恢復環(huán)境,給tmp賦值(注意:這里的tmp的值并不是x的值,而是通過send方法接受的值),由于我們沒有調(diào)用send方法,所以 tmp 的值為None,此時輸出None,并執(zhí)行到下一次yield x,所以又輸出1。
到了這里,next方法我們都懂了,下面看看send方法。
print(g.send('hello')) # world??2?? ? ? 上一次執(zhí)行到 yield 1后暫停,此時我們send('hello'),那么程序?qū)⑹盏健甴ello',并給tmp賦值為’hello',此時tmp=='hello'為真,所以輸出'world',并執(zhí)行到下一次yield 2,所以又打印出2.(next()等價于send(None))
? ? ? 當循環(huán)結束,將拋出StopIteration停止生成器。
? ? ? 看下面代碼:
def stop_immediately(name):if name == 'sky_crab':yield 'ok_ok'else:print('no_no')s = stop_immediately('sky')# s.next() # 在 python 3.x中 generator(有yield關鍵字的函數(shù)則會被識別為generator函數(shù))中的next變?yōu)開_next__了 # next() 是 python 3.x 以前版本中的方法, 在 python3 中使用 __next__() 代替 s.__next__()正如你所預料的,打印出’no_no',由于沒有額外的 yield,所以將直接拋出StopIteration。
Traceback (most recent call last): no_noFile "E:/taopiao/text_3.py", line 32, in <module>s.__next__() StopIteration看下面代碼,理解throw方法,throw主要是向生成器發(fā)送異常。
def mygen():try:yield 'something'except ValueError:yield 'value error'finally:print('clean') # 一定會被執(zhí)行gg = mygen() # print(gg.next()) # something # print(gg.throw(ValueError)) # value error cleanprint(gg.__next__()) # something print(gg.throw(ValueError)) # value error clean? ? ? ? 調(diào)用 gg.next 很明顯此時輸出 ‘something’ ,并在yield ‘something’暫停,此時向gg發(fā)送ValueError異常,恢復執(zhí)行環(huán)境,except ?將會捕捉,并輸出信息。
? ? ? ? 理解了這些,我們就可以向協(xié)同程序發(fā)起攻擊了,所謂協(xié)同程序也就是是可以掛起,恢復,有多個進入點。其實說白了,也就是說多個函數(shù)可以同時進行,可以相互之間發(fā)送消息等。??
? ? ? ? 這里有必要說一下multitask模塊 ( 不是標準庫模塊,https://www.cnblogs.com/djw316/p/11734210.html) 看一段 multitask 使用的簡單代碼:
def tt():for x in range(4):print('tt' + str(x))yielddef gg():for x in range(4):print('xx' + str(x))yieldt = multitask.TaskManager() t.add(tt()) t.add(gg()) t.run()結果:
tt0 xx0 tt1 xx1 tt2 xx2 tt3 xx3下載地址:https://pypi.org/search/?q=multitask,pypi 地址:https://pypi.org/project/python-multitasking/
安裝:pip install python-multitasking? ?。使用示例:
import multitasking import time import random import requests import signal import urllib3urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)# kill all tasks on ctrl-c signal.signal(signal.SIGINT, multitasking.killall)# or, wait for task to finish on ctrl-c: # signal.signal(signal.SIGINT, multitasking.wait_for_tasks)@multitasking.task # <== this is all it takes :-) def hello(index):global global_list_flagurl = 'https://www.baidu.com/'print(f'{index} : {url}')req = requests.get(url, verify=False)print(f'{index} : {req.status_code}')global_list_flag[index-1] = 1if __name__ == "__main__":count = 10global_list_flag = [0 for _ in range(count)]for i in range(0, count):hello(i + 1)# https://www.yuanrenxue.com/python/python-asyncio-demo.htmlmultitasking.wait_for_tasks()如果不是使用生成器,那么要實現(xiàn)上面現(xiàn)象,即函數(shù)交錯輸出,那么只能使用線程了,所以生成器給我們提供了更廣闊的前景。?
如果僅僅是實現(xiàn)上面的效果,其實很簡單,我們可以自己寫一個。主要思路就是將生成器對象放入隊列,執(zhí)行send(None)后,如果沒有拋出StopIteration,將該生成器對象再加入隊列。
# python 2.X 叫 Queue # python 3.X 叫 queue import queue import multitaskdef tt():for x in range(4):print('tt' + str(x))yielddef gg():for x in range(4):print('xx' + str(x))yieldclass Task(object):def __init__(self):self._queue = queue.Queue()def add(self, gen):self._queue.put(gen)def run(self):while not self._queue.empty():for i in range(self._queue.qsize()):try:gen = self._queue.get()gen.send(None)except StopIteration:passelse:self._queue.put(gen)t = Task() t.add(tt()) t.add(gg()) t.run()當然,multitask 實現(xiàn)的肯定不止這個功能,有興趣的童鞋可以看下源碼,還是比較簡單易懂的。
?
有這么一道題目,模擬多線程交替輸出:
def thread1():for x in range(4):yield xdef thread2():for x in range(4, 8):yield xthreads = [] threads.append(thread1()) threads.append(thread2())def run(threads): # 寫這個函數(shù),模擬線程并發(fā)passrun(threads)如果上面 class Task 看懂了,那么這題很簡單,其實就是考你用yield模擬線程調(diào)度,解決如下:
def thread1():for x in range(4):yield xdef thread2():for x in range(4, 8):yield xtd_list = list() td_list.append(thread1()) td_list.append(thread2())def run(thread_list):for td in thread_list:try:print(next(td))except StopIteration:passelse:thread_list.append(td)run(td_list)?
?
3.itertools
?
itertools模塊包含了許多用來操作可迭代對象的函數(shù)。
想復制一個生成器?想連接兩個生成器?想把多個值組合到一個嵌套列表里面?使用?map/zip?而不用重新創(chuàng)建一個列表?那么就:import itertools?吧。
讓我們來看看四匹馬賽跑可能的到達結果:
>>> horses = [1, 2, 3, 4] >>> races = itertools.permutations(horses) >>> print(races) <itertools.permutations object at 0xb754f1dc> >>> print(list(itertools.permutations(horses))) [(1, 2, 3, 4),(1, 2, 4, 3),(1, 3, 2, 4),(1, 3, 4, 2),(1, 4, 2, 3),(1, 4, 3, 2),(2, 1, 3, 4),(2, 1, 4, 3),(2, 3, 1, 4),(2, 3, 4, 1),(2, 4, 1, 3),(2, 4, 3, 1),(3, 1, 2, 4),(3, 1, 4, 2),(3, 2, 1, 4),(3, 2, 4, 1),(3, 4, 1, 2),(3, 4, 2, 1),(4, 1, 2, 3),(4, 1, 3, 2),(4, 2, 1, 3),(4, 2, 3, 1),(4, 3, 1, 2),(4, 3, 2, 1)]迭代的內(nèi)部機理:
? ? ? ? 迭代是一個依賴于可迭代對象(需要實現(xiàn)__iter__()方法)和迭代器(需要實現(xiàn)__next__()方法)的過程。
? ? ? ? 可迭代對象是任意你可以從中得到一個迭代器的對象。
? ? ? ? 迭代器是讓你可以對可迭代對象進行迭代的對象。
?
?
4. 總? 結
?
可迭代對象(Iterable)是實現(xiàn)了__iter__()方法的對象,通過調(diào)用iter()方法可以獲得一個迭代器(Iterator)。
迭代器(Iterator)是實現(xiàn)了__iter__()和__next__()的對象。
for ... in ...的迭代,實際是將可迭代對象轉(zhuǎn)換成迭代器,再重復調(diào)用next()方法實現(xiàn)的。
生成器(generator)是一個特殊的迭代器,它的實現(xiàn)更簡單優(yōu)雅
yield?是生成器實現(xiàn)__next__()方法的關鍵。它作為生成器執(zhí)行的暫停恢復點,可以對yield表達式進行賦值,也可以將yield表達式的值返回。
?
yield?語句將你的函數(shù)轉(zhuǎn)化成一個能夠生成一種能夠包裝你原函數(shù)體的名叫生成器?的特殊對象的工廠。
當生成器被迭代時,它將會從起始位置開始執(zhí)行函數(shù)一直到達下一個yield,然后掛起執(zhí)行,計算返回傳遞給yield的值,它將會在每次迭代的時候重復這個過程直到函數(shù)執(zhí)行到達函數(shù)的尾部,舉例來說:
def simple_generator():yield 'one'yield 'two'yield 'three' for i in simple_generator():print i輸出結果為: one two three這種效果的產(chǎn)生是由于在循環(huán)中使用了可以產(chǎn)生序列的生成器,生成器在每次循環(huán)時執(zhí)行代碼到下一個yield,并計算返回結果,這樣生成器即時生成了一個列表,這對于特別是大型計算來說內(nèi)存節(jié)省十分有效。
假設你想實現(xiàn)自己的可以產(chǎn)生一個可迭代一定范圍數(shù)的range函數(shù)(特指Python 2.x中的range),你可以這樣做和使用:
def myRangeNaive(i):n = 0range = []while n < i:range.append(n)n = n + 1return range for i in myRangeNaive(10):print i但是這樣并不高效,原因1:你創(chuàng)建了一個你只會使用一次的列表;原因2:這段代碼實際上循環(huán)了兩次。
由于Guido和他的團隊很慷慨地開發(fā)了生成器因此我們可以這樣做:
現(xiàn)在,每次對生成器迭代將會調(diào)用next()來執(zhí)行函數(shù)體直到到達yield語句,然后停止執(zhí)行,并計算返回結果,或者是到達函數(shù)體尾部。在這種情況下,第一次的調(diào)用next()將會執(zhí)行到yield n并返回n,下一次的next()將會執(zhí)行自增操作,然后回到while的判斷,如果滿足條件,則再一次停止并返回n,它將會以這種方式執(zhí)行一直到不滿足while條件,使得生成器到達函數(shù)體尾部。
?
?
5. 提高你的 Python: 解釋 yield 和 Generators(生成器)
?
來源:?https://www.oschina.net/translate/improve-your-python-yield-and-generators-explained
? ? ? ? 我們調(diào)用一個普通的Python函數(shù)時,一般是從函數(shù)的第一行代碼開始執(zhí)行,結束于return語句、異常或者函數(shù)結束(可以看作隱式的返回None)。一旦函數(shù)將控制權交還給調(diào)用者,就意味著全部結束。函數(shù)中做的所有工作以及保存在局部變量中的數(shù)據(jù)都將丟失。再次調(diào)用這個函數(shù)時,一切都將從頭創(chuàng)建。?
? ? ? ? 對于在計算機編程中所討論的函數(shù),這是很標準的流程。這樣的函數(shù)只能返回一個值,不過,有時可以創(chuàng)建能產(chǎn)生一個序列的函數(shù)還是有幫助的。要做到這一點,這種函數(shù)需要能夠“保存自己的工作”。?
? ? ? ? 我說過,能夠“產(chǎn)生一個序列”是因為我們的函數(shù)并沒有像通常意義那樣返回。return隱含的意思是函數(shù)正將執(zhí)行代碼的控制權返回給函數(shù)被調(diào)用的地方。而"yield"的隱含意思是控制權的轉(zhuǎn)移是臨時和自愿的,我們的函數(shù)將來還會收回控制權。
? ? ? 在Python中,擁有這種能力的“函數(shù)”被稱為生成器,它非常的有用。生成器(以及yield語句)最初的引入是為了讓程序員可以更簡單的編寫用來產(chǎn)生值的序列的代碼。 以前,要實現(xiàn)類似隨機數(shù)生成器的東西,需要實現(xiàn)一個類或者一個模塊,在生成數(shù)據(jù)的同時保持對每次調(diào)用之間狀態(tài)的跟蹤。引入生成器之后,這變得非常簡單。
為了更好的理解生成器所解決的問題,讓我們來看一個例子。在了解這個例子的過程中,請始終記住我們需要解決的問題:生成值的序列。
? ? ? 生成器。一個生成器會 “生成” 值。創(chuàng)建一個生成器幾乎和生成器函數(shù)的原理一樣簡單。
一個生成器函數(shù)的定義很像一個普通的函數(shù),除了當它要生成一個值的時候,使用yield關鍵字而不是return。如果一個def的主體包含yield,這個函數(shù)會自動變成一個生成器(即使它包含一個return)。除了以上內(nèi)容,創(chuàng)建一個生成器沒有什么多余步驟了。
生成器函數(shù)返回生成器的迭代器。這可能是你最后一次見到“生成器的迭代器”這個術語了, 因為它們通常就被稱作“生成器”。要注意的是生成器就是一類特殊的迭代器。作為一個迭代器,生成器必須要定義一些方法(method),其中一個就是__next__()。如同迭代器一樣,我們可以使用next()函數(shù)來獲取下一個值。
為了從生成器獲取下一個值,我們使用next()函數(shù),就像對付迭代器一樣。( next()會操心如何調(diào)用生成器的__next__()方法 )。
既然生成器是一個迭代器,它可以被用在for循環(huán)中。
每當生成器被調(diào)用的時候,它會返回一個值給調(diào)用者。其實 yield 就是專門給生成器使用的 return (加上點小魔法)。
下面是一個簡單的生成器函數(shù):
>>> def simple_generator_function(): >>> yield 1 >>> yield 2 >>> yield 3這里有兩個簡單的方法來使用它:
>>> for value in simple_generator_function(): >>> print(value) 1 2 3 >>> our_generator = simple_generator_function() >>> next(our_generator) 1 >>> next(our_generator) 2 >>> next(our_generator) 3那么神奇的部分在哪里?我很高興你問了這個問題!當一個生成器函數(shù)調(diào)用yield,生成器函數(shù)的“狀態(tài)”會被凍結,所有的變量的值會被保留下來,下一行要執(zhí)行的代碼的位置也會被記錄,直到再次調(diào)用next()。一旦next()再次被調(diào)用,生成器函數(shù)會從它上次離開的地方開始。如果永遠不調(diào)用next(),yield保存的狀態(tài)就被無視了。
我們來重寫get_primes()函數(shù),這次我們把它寫作一個生成器。注意我們不再需要magical_infinite_range函數(shù)了。使用一個簡單的while循環(huán),我們創(chuàng)造了自己的無窮串列。
def get_primes(number):while True:if is_prime(number):yield numbernumber += 1如果生成器函數(shù)調(diào)用了return,或者執(zhí)行到函數(shù)的末尾,會出現(xiàn)一個StopIteration異常。 這會通知next()的調(diào)用者這個生成器沒有下一個值了(這就是普通迭代器的行為)。這也是這個while循環(huán)在我們的get_primes()函數(shù)出現(xiàn)的原因。如果沒有這個while,當我們第二次調(diào)用next()的時候,生成器函數(shù)會執(zhí)行到函數(shù)末尾,觸發(fā)StopIteration異常。一旦生成器的值用完了,再調(diào)用next()就會出現(xiàn)錯誤,所以你只能將每個生成器的使用一次。下面的代碼是錯誤的:
>>> our_generator = simple_generator_function() >>> for value in our_generator: >>> print(value)>>> # 我們的生成器沒有下一個值了... >>> print(next(our_generator)) Traceback (most recent call last):File "<ipython-input-13-7e48a609051a>", line 1, in <module>next(our_generator) StopIteration>>> # 然而,我們總可以再創(chuàng)建一個生成器 >>> # 只需再次調(diào)用生成器函數(shù)即可>>> new_generator = simple_generator_function() >>> print(next(new_generator)) # 工作正常 1因此,這個while循環(huán)是用來確保生成器函數(shù)永遠也不會執(zhí)行到函數(shù)末尾的。只要調(diào)用next()這個生成器就會生成一個值。這是一個處理無窮序列的常見方法(這類生成器也是很常見的)。
?
5.1 執(zhí)行流程
讓我們回到調(diào)用 get_primes 的地方:solve_number_10。
def solve_number_10():# She *is* working on Project Euler #10, I knew it!total = 2for next_prime in get_primes(3):if next_prime < 2000000:total += next_primeelse:print(total)return我們來看一下solve_number_10的for循環(huán)中對get_primes的調(diào)用,觀察一下前幾個元素是如何創(chuàng)建的有助于我們的理解。當for循環(huán)從get_primes請求第一個值時,我們進入get_primes,這時與進入普通函數(shù)沒有區(qū)別。
接下來,回到insolve_number_10:
這次,進入get_primes時并沒有從開頭執(zhí)行,我們從第5行繼續(xù)執(zhí)行,也就是上次離開的地方。
def get_primes(number):while True:if is_prime(number):yield numbernumber += 1 # <<<<<<<<<<最關鍵的是,number還保持我們上次調(diào)用yield時的值(例如3)。記住,yield會將值傳給next()的調(diào)用方,同時還會保存生成器函數(shù)的“狀態(tài)”。接下來,number加到4,回到while循環(huán)的開始處,然后繼續(xù)增加直到得到下一個素數(shù)(5)。我們再一次把number的值通過yield返回給solve_number_10的for循環(huán)。這個周期會一直執(zhí)行,直到for循環(huán)結束(得到的素數(shù)大于2,000,000)。
?
更給力點
在PEP 342中加入了將值傳給生成器的支持。PEP 342加入了新的特性,能讓生成器在單一語句中實現(xiàn),生成一個值(像從前一樣),接受一個值,或同時生成一個值并接受一個值。
我們用前面那個關于素數(shù)的函數(shù)來展示如何將一個值傳給生成器。這一次,我們不再簡單地生成比某個數(shù)大的素數(shù),而是找出比某個數(shù)的等比級數(shù)大的最小素數(shù)(例如10, 我們要生成比10,100,1000,10000 ... 大的最小素數(shù))。我們從get_primes開始:
def print_successive_primes(iterations, base=10):# 像普通函數(shù)一樣,生成器函數(shù)可以接受一個參數(shù)prime_generator = get_primes(base)# 這里以后要加上點什么for power in range(iterations):# 這里以后要加上點什么def get_primes(number):while True:if is_prime(number):# 這里怎么寫?get_primes的后幾行需要著重解釋。yield關鍵字返回number的值,而像 other = yield foo 這樣的語句的意思是,"返回foo的值,這個值返回給調(diào)用者的同時,將other的值也設置為那個值"。你可以通過send方法來將一個值”發(fā)送“給生成器。
def get_primes(number):while True:if is_prime(number):number = yield numbernumber += 1通過這種方式,我們可以在每次執(zhí)行yield的時候為number設置不同的值。現(xiàn)在我們可以補齊print_successive_primes中缺少的那部分代碼:
def print_successive_primes(iterations, base=10):prime_generator = get_primes(base)prime_generator.send(None)for power in range(iterations):print(prime_generator.send(base ** power))這里有兩點需要注意:首先,我們打印的是generator.send的結果,這是沒問題的,因為send在發(fā)送數(shù)據(jù)給生成器的同時還返回生成器通過yield生成的值(就如同生成器中yield語句做的那樣)。
第二點,看一下prime_generator.send(None)這一行,當你用send來“啟動”一個生成器時(就是從生成器函數(shù)的第一行代碼執(zhí)行到第一個yield語句的位置),你必須發(fā)送None。這不難理解,根據(jù)剛才的描述,生成器還沒有走到第一個yield語句,如果我們發(fā)生一個真實的值,這時是沒有人去“接收”它的。一旦生成器啟動了,我們就可以像上面那樣發(fā)送數(shù)據(jù)了。
?
綜述
在本系列文章的后半部分,我們將討論一些yield的高級用法及其效果。yield已經(jīng)成為Python最強大的關鍵字之一。現(xiàn)在我們已經(jīng)對yield是如何工作的有了充分的理解,我們已經(jīng)有了必要的知識,可以去了解yield的一些更“費解”的應用場景。
不管你信不信,我們其實只是揭開了yield強大能力的一角。例如,send確實如前面說的那樣工作,但是在像我們的例子這樣,只是生成簡單的序列的場景下,send幾乎從來不會被用到。下面我貼一段代碼,展示send通常的使用方式。對于這段代碼如何工作以及為何可以這樣工作,在此我并不打算多說,它將作為第二部分很不錯的熱身。
import randomdef get_data():"""返回0到9之間的3個隨機數(shù)"""return random.sample(range(10), 3)def consume():"""顯示每次傳入的整數(shù)列表的動態(tài)平均值"""running_sum = 0data_items_seen = 0while True:data = yielddata_items_seen += len(data)running_sum += sum(data)print('The running average is {}'.format(running_sum / float(data_items_seen)))def produce(consumer):"""產(chǎn)生序列集合,傳遞給消費函數(shù)(consumer)"""while True:data = get_data()print('Produced {}'.format(data))consumer.send(data)yieldif __name__ == '__main__':consumer = consume()consumer.send(None)producer = produce(consumer)for _ in range(10):print('Producing...')next(producer)?
?
?
總結
以上是生活随笔為你收集整理的Python 生成器 和 yield 关键字的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Google Pixel 解锁BL、刷入
- 下一篇: Python 多进程 multiproc