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與實現:http://www.cnblogs.com/coder2012/p/4990834.html
?
?
1. 迭代(iteration)、迭代器(iterator)、generator(生成器)
?
1.1 迭代
?
為了搞清楚 yield 是用來做什么的,首先得知道 Python 中 generator(生成器) 的相關概念,要理解 generator(生成器) ,的先從 迭代(iteration) 與 迭代器(iterator) 講起。
迭代是重復反饋過程的活動,其目的通常是為了接近并到達所需的目標或結果。每一次對過程的重復被稱為一次“迭代”,而每一次迭代得到的結果會被用來作為下一次迭代的初始值?!? 以上是?維基百科?對迭代的定義。
在 Python中,迭代 通常是通過?for ... in ...來完成的,而且只要是?可迭代對象(iterable),都能進行迭代。
當你創建一個了列表,你可以逐個遍歷列表中的元素,而這個過程便叫做?迭代 。
>>> mylist = [1, 2, 3] >>> for i in mylist: ... print(i) 1 2 3而?mylist是一個可迭代對象。當你使用列表推導式的時候,創建了一個列表,他也是可迭代對象:
>>> mylist = [x*x for x in range(3)] >>> for i in mylist: ... print(i) 0 1 4所有能夠接受?for...in...操作的對象都是 可迭代對象,如:列表、字符串、文件等。這些可迭代對象用起來都十分順手。?因為你可以按照你的想法去訪問它們,但是你把所有數據都保存在了內存中,而當你有大量數據的時候這可能并不是你想要的結果。
?
1.2 可迭代對象(iterable) 與 迭代器(iterator)
?
這里簡單講下??可迭代對象(iterable)與 迭代器(iterator):
- 1.可迭代對象(iterable)?是實現了__iter__()方法的對象。更確切的說,是container.__iter__()方法,該方法返回的是的一個iterator對象,因此iterable是你可以從其獲得iterator的對象。對于iterable,重點關注的是:它是一個能一次返回一個成員的對象(iterable is an object capable of returning its members one at a time),一些?iterable?將所有值都存儲在內存中,比如list,而另一些并不是這樣,比如我們下面將講到的?iterator.
- 2. 迭代器(iterator)?是實現了iterator.__iter__()?和?iterator.__next__()方法的對象。iterator.__iter__()方法返回的是iterator對象本身。根據官方的說法,正是這個方法,實現了for ... in ...語句。而?iterator.__next__()是??iterator?區別于?iterable?的關鍵,它允許我們?顯式?地獲取一個元素。當調用?next()方法時,實際上產生了2個操作:?
? ? ? ? 1. 更新 iterator 狀態,令其指向后一項,以便下一次調用
? ? ? ? 2. 返回當前結果
如果你學過?C++,它其實跟指針的概念很像(如果你還學過鏈表的話,或許能更好地理解)。
正是?__next__(),使得iterator能在每次被調用時,返回一個單一的值(有些教程里,稱為一邊循環,一邊計算,我覺得這個說法不是太準確。但如果這樣的說法有助于你的理解,我建議你就這樣記),從而極大的節省了內存資源。另一點需要格外注意的是,iterator是消耗型的,即每一個值被使用過后,就消失了。因此,你可以將以上的操作2理解成pop。對iterator進行遍歷之后,其就變成了一個空的容器了,但不等于None哦。因此,若要重復使用iterator,利用list()方法將其結果保存起來是一個不錯的選擇。
我們通過代碼來感受一下。
>>> from collections import Iterable, Iterator >>> a = [1,2,3] # 眾所周知,list是一個iterable >>> b = iter(a) # 通過iter()方法,得到iterator,iter()實際上調用了__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 已經遍歷過了,所以 d 就為 空 了 >>> d = list(b) >>> d []空的 iterator 并不等于 None。
>>> if b: ... print(1) ... 1 >>> if b == None: ... print(1) ...再來感受一下 next()
>>> e = iter(a) >>> next(e) #next()實際調用了__next__()方法,此后不再多說 1 >>> next(e) 2既然提到了for ... in ...語句,我們再來簡單講下其工作原理吧,或許能幫助理解以上所講的內容。
>>> x = [1, 2, 3] >>> for i in x: ... ...我們對一個iterable用for ... in ...進行迭代時,實際是先通過調用iter()方法得到一個iterator,假設叫做X。然后循環地調用X的next()方法取得每一次的值,直到iterator為空,返回的StopIteration作為循環結束的標志。for ... in ...會自動處理StopIteration異常,從而避免了拋出異常而使程序中斷。如圖所示
磨刀不誤砍柴工,有了前面的知識,我們再來理解?generator?與?yield?將會事半功倍。
?
?
2. 生成器
?
生成器 是 這樣一個函數,它記住上一次返回時在函數體中的位置。
對生成器函數的第二次(或第 n 次)調用跳轉至該函數中間,而上次調用的所有局部變量都保持不變。
生成器不僅 “記住” 了它的數據狀態;生成器還 “記住” 了它的流控制構造(在命令式編程中,這種構造不只是數據值)中的位置。?
生成器的特點:
- 1. 生成器是一個函數,而且函數的參數都會保留。
- 2. 迭代到下一次的調用時,所使用的參數都是第一次所保留下的,即是說,在整個所有函數調用的參數都是第一次所調用時保留的,而不是新創建的
- 3. 節約內存
- 一個生成器函數的定義很像一個普通的函數,除了當它要生成一個值的時候,使用 yield 關鍵字而不是 return。如果一個 def 的主體包含 yield,這個函數會自動變成一個生成器(即使它包含一個 return)。創建一個生成器就這么簡單。。。
生成器 比 迭代器 更加強大也更加復雜,需要花點功夫好好理解貫通。先理清幾個概念:
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官方文檔??梢?#xff0c;我們常說的生成器,就是帶有?yield?的函數,而?generator iterator?則是generator function?的返回值,即一個generator對象,而形如(elem for elem in [1, 2, 3])的表達式,稱為generator expression,實際使用與generator無異。
生成器 也是 迭代器,但是你只能對它們進行一次迭代,原因在于它們并沒有將所有數據存儲在內存中,而是即時生成這些數據:
>>> 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?的一種,以更優雅的方式實現的?iterator。
官方的說法是:Python’s generators provide a convenient way to implement the iterator protocol.
你完全可以像使用?iterator?一樣使用?generator,但是請記住他們兩個的定義不一樣:
- 定義一個?iterator,你需要分別實現?__iter__()?方法和?__next__()?方法,
- 但?generator?只需要一個小小的?yield?( 好吧,generator expression?的使用比較簡單,就不展開講了。)
?
?
3. yield 關鍵字
?
Python 中 生成器 是使用? yield 關鍵字 來實現的。
- 1. yield?是一個用法跟?return?很相似的關鍵字,但是使用yield的函數返回的是一個 生成器。
- 2. yield 可以暫停一個函數并返回中間結果。使用 yield 的函數 將 保存執行環境,即函數的參數都會保留,并且在必要時恢復。到下一次的調用時,所有參數都恢復,所使用的參數都是第一次所保留下的參數和環境,然后從先前暫停的地方開始執行,直到遇到下一個 yield 再次 暫停。
看一段代碼:
def test_func():for i in range(5):yield i #print(i + 100)t = test_func() for i in t:print(i)pass可以 單步調試 上面這個代碼,就可以 驗證 上面 兩個 特點。
前文講到?iterator?通過?__next__()方法實現了每次調用,返回一個單一值的功能。而?yield?就是實現?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解釋:
- 第一次調用?next()方法時,函數似乎執行到?yield 1,就暫停了。
? ? (執行next方法,使程序運行到 yield 處暫停并返回1,然后交出控制權,然后 next 接收控制權,并獲得 yield 的返回值 1) - 第二次調用?next()方法時,函數從?yield 1之后開始執行的,并再次暫停。
- 第三次調用?next()方法時,從第二次暫停的地方開始執行。
- 第四次調用?next()方法時,拋出StopIteration?異常。
事實上,generator?確實在遇到?yield?之后暫停了,確切點說,是先返回了?yield?表達式的值,再暫停的。當再次調用?next()時,從先前暫停的地方開始執行,直到遇到下一個?yield。這與上文介紹的對iterator調用next()方法,執行原理一般無二。
有些教程里說?generator?保存的是算法,而我覺得用?中斷服務子程序?來描述?generator?或許能更好理解,這樣你就能將 yield?理解成一個中斷服務子程序的?斷點,沒錯,是中斷服務子程序的斷點。我們每次對一個?generator對象調用?next()時,函數內部代碼執行到 "斷點"?yield,然后返回這一部分的結果,并保存上下文環境,"中斷" 返回。
怎么樣,是不是瞬間就明白了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 種調用方法(恢復執行),即通過?send(value)?方法將?value?作為?yield?表達式的當前值,你可以用該值再對其他變量進行賦值,這一段代碼就很好理解了。當我們調用send(value)方法時,generator正由于yield的緣故被暫停了。此時,send(value)?方法傳入的值作為?yield?表達式的值,函數中又將該值賦給了變量?s,然后 print 函數打印?s,循環再遇到yield,暫停返回。
調用?send(value)?時要注意,要確保?generator?是在?yield?處被暫停了,如此才能向?yield表達式傳值,否則將會報錯(如上所示),可通過?next()方法或?send(None)使?generator執行到?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)# 調用send(value)時要注意,要確保generator是在yield處被暫停了, # 如此才能向yield表達式傳值,否則將會報錯 # 可通過next()方法或send(None)使generator執行到yield。 # 生成器(generator) 有兩種方法 恢復執行:1. send() 方法。2. next() 方法g = echo(1) # 返回一個 生成器 print(next(g)) # 通過 next() 方法 使 生成器 執行到 yield 處暫停 g.send(2) # send(value)方法傳入的值作為yield表達式的值 g.send(5) next(g) next(g) next(g) """ 執行結果: 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?分離代碼進行解讀,就不太難了。
- 第一次調用?next()方法,執行到?yield value表達式,保存上下文環境暫停返回?1。
- 第二次調用?send(value)方法,從?value = yield?開始,打印,再次遇到?yield value?暫停返回。
- 后續的調用?send(value) 或?next()都是如此。
但是,這里就引出了另一個問題,yield?作為一個暫?;謴偷狞c,代碼從?yield?處恢復,又在下一個?yield?處暫停??梢?#xff0c;在一次?next()(非首次) 或?send(value)調用過程中,實際上存在?2?個?yield
- 一個作為恢復點的?yield
- 一個作為暫停點的yield
因此,也就有 2 個?yield 表達式。send(value)方法是將值傳給恢復點yield。調用next()表達式的值時,其恢復點yield的值總是為None,而將暫停點的yield表達式的值返回。為方便記憶,你可以將此處的恢復點記作當前的(current),而將暫停點記作下一次的(next),這樣就與next()方法匹配起來啦。
generator還實現了另外兩個方法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這是一個沒有什么用的例子,但是用來讓你了解當你知道你的函數會返回一個只會被遍歷1次的巨大數據集合該怎么做的時候十分方便。為了掌握yield,你必須了解當你調用這個函數的時候,你在函數體中寫的代碼并沒有被執行,而是只返回了一個生成器對象,這個需要特別注意。然后,你的代碼將會在每次for使用這個生成器的時候被執行。最后,最困難的部分:
for第一次調用通過你函數創建的生成器對象的時候,它將會從你函數的開頭執行代碼,一直到到達yield,然后它將會返回循環中的第一個值。然后,其他每次調用都會再一次執行你在函數中寫的那段循環,并返回下一個值,直到沒有值可以返回。
生成器在函數執行了卻沒有到達yield的時候將被認為是空的,原因在于循環到達了終點,或者不再滿足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方法,它用來執行代碼并從生成器中獲取下一個元素(在Python 3.x中生成器已經沒有next方法,而是使用next(iterator)代替)。在crisis未被置為True的時候,create_atm函數中的while循環可以看做是無盡的,當crisis為True的時候,跳出了while循環,所有迭代器將會到達函數尾部,此時再次訪問next將會拋出StopIteration異常,而此時就算將crisis設置為False,這些生成器仍然處在函數尾部,訪問會繼續拋出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執行到 yield 時,gen 函數暫時停止并保存,返回x的值,同時tmp接收send的值(ps:yield x 相當于 return x ,所以第一次c.next()結果是0。第二次c.next()時,繼續在原來暫停的地方執行,因為沒有send 值,所以tmp 為 None。c.next()等價c.send(None))。下次c.send(“python”),send發送過來的值,c.next()等價c.send(None)
了解了next()如何讓包含yield的函數執行后,我們再來看另外一個非常重要的函數send(msg)。其實next()和send()在一定意義上作用是相似的,區別是send()可以傳遞yield表達式的值進去,而next()不能傳遞特定的值,只能傳遞None進去。因此,我們可以看做c.next() 和 c.send(None) 作用是一樣的。
需要提醒的是,第一次調用時,請使用next()語句或是send(None),不能使用send發送一個非None的值,否則會出錯的,因為沒有Python?yield語句來接收這個值。
?
示例解析:
def gen():for x in range(4):tmp = yield xif tmp == 'hello':print('world')else:print(str(tmp))只要函數中包含yield關鍵字,該函數調用就是生成器對象。
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() 并不是函數調用,而是產生生成器對象。
生成器對象支持幾個方法,如 gen.next() ,gen.send() ,gen.throw() 等。
print(g.next()) # 0調用生成器的 next 方法,將運行到 yield 位置,此時暫停執行環境,并返回yield后的值。所以打印出的是0,暫停執行環境。
print(g.next())?# None??1再調用 next 方法,你也許會好奇,為啥打印出兩個值,不急,且聽我慢慢道來:上一次調用 next,執行到 yield 0暫停,再次執行恢復環境,給tmp賦值(注意:這里的tmp的值并不是x的值,而是通過send方法接受的值),由于我們沒有調用send方法,所以 tmp 的值為None,此時輸出None,并執行到下一次yield x,所以又輸出1。
到了這里,next方法我們都懂了,下面看看send方法。
print(g.send('hello')) # world??2?? ? ? 上一次執行到 yield 1后暫停,此時我們send('hello'),那么程序將收到‘hello',并給tmp賦值為’hello',此時tmp=='hello'為真,所以輸出'world',并執行到下一次yield 2,所以又打印出2.(next()等價于send(None))
? ? ? 當循環結束,將拋出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關鍵字的函數則會被識別為generator函數)中的next變為__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主要是向生成器發送異常。
def mygen():try:yield 'something'except ValueError:yield 'value error'finally:print('clean') # 一定會被執行gg = mygen() # print(gg.next()) # something # print(gg.throw(ValueError)) # value error cleanprint(gg.__next__()) # something print(gg.throw(ValueError)) # value error clean? ? ? ? 調用 gg.next 很明顯此時輸出 ‘something’ ,并在yield ‘something’暫停,此時向gg發送ValueError異常,恢復執行環境,except ?將會捕捉,并輸出信息。
? ? ? ? 理解了這些,我們就可以向協同程序發起攻擊了,所謂協同程序也就是是可以掛起,恢復,有多個進入點。其實說白了,也就是說多個函數可以同時進行,可以相互之間發送消息等。??
? ? ? ? 這里有必要說一下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()如果不是使用生成器,那么要實現上面現象,即函數交錯輸出,那么只能使用線程了,所以生成器給我們提供了更廣闊的前景。?
如果僅僅是實現上面的效果,其實很簡單,我們可以自己寫一個。主要思路就是將生成器對象放入隊列,執行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 實現的肯定不止這個功能,有興趣的童鞋可以看下源碼,還是比較簡單易懂的。
?
有這么一道題目,模擬多線程交替輸出:
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): # 寫這個函數,模擬線程并發passrun(threads)如果上面 class Task 看懂了,那么這題很簡單,其實就是考你用yield模擬線程調度,解決如下:
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模塊包含了許多用來操作可迭代對象的函數。
想復制一個生成器?想連接兩個生成器?想把多個值組合到一個嵌套列表里面?使用?map/zip?而不用重新創建一個列表?那么就: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)]迭代的內部機理:
? ? ? ? 迭代是一個依賴于可迭代對象(需要實現__iter__()方法)和迭代器(需要實現__next__()方法)的過程。
? ? ? ? 可迭代對象是任意你可以從中得到一個迭代器的對象。
? ? ? ? 迭代器是讓你可以對可迭代對象進行迭代的對象。
?
?
4. 總? 結
?
可迭代對象(Iterable)是實現了__iter__()方法的對象,通過調用iter()方法可以獲得一個迭代器(Iterator)。
迭代器(Iterator)是實現了__iter__()和__next__()的對象。
for ... in ...的迭代,實際是將可迭代對象轉換成迭代器,再重復調用next()方法實現的。
生成器(generator)是一個特殊的迭代器,它的實現更簡單優雅
yield?是生成器實現__next__()方法的關鍵。它作為生成器執行的暫停恢復點,可以對yield表達式進行賦值,也可以將yield表達式的值返回。
?
yield?語句將你的函數轉化成一個能夠生成一種能夠包裝你原函數體的名叫生成器?的特殊對象的工廠。
當生成器被迭代時,它將會從起始位置開始執行函數一直到達下一個yield,然后掛起執行,計算返回傳遞給yield的值,它將會在每次迭代的時候重復這個過程直到函數執行到達函數的尾部,舉例來說:
def simple_generator():yield 'one'yield 'two'yield 'three' for i in simple_generator():print i輸出結果為: one two three這種效果的產生是由于在循環中使用了可以產生序列的生成器,生成器在每次循環時執行代碼到下一個yield,并計算返回結果,這樣生成器即時生成了一個列表,這對于特別是大型計算來說內存節省十分有效。
假設你想實現自己的可以產生一個可迭代一定范圍數的range函數(特指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:你創建了一個你只會使用一次的列表;原因2:這段代碼實際上循環了兩次。
由于Guido和他的團隊很慷慨地開發了生成器因此我們可以這樣做:
現在,每次對生成器迭代將會調用next()來執行函數體直到到達yield語句,然后停止執行,并計算返回結果,或者是到達函數體尾部。在這種情況下,第一次的調用next()將會執行到yield n并返回n,下一次的next()將會執行自增操作,然后回到while的判斷,如果滿足條件,則再一次停止并返回n,它將會以這種方式執行一直到不滿足while條件,使得生成器到達函數體尾部。
?
?
5. 提高你的 Python: 解釋 yield 和 Generators(生成器)
?
來源:?https://www.oschina.net/translate/improve-your-python-yield-and-generators-explained
? ? ? ? 我們調用一個普通的Python函數時,一般是從函數的第一行代碼開始執行,結束于return語句、異常或者函數結束(可以看作隱式的返回None)。一旦函數將控制權交還給調用者,就意味著全部結束。函數中做的所有工作以及保存在局部變量中的數據都將丟失。再次調用這個函數時,一切都將從頭創建。?
? ? ? ? 對于在計算機編程中所討論的函數,這是很標準的流程。這樣的函數只能返回一個值,不過,有時可以創建能產生一個序列的函數還是有幫助的。要做到這一點,這種函數需要能夠“保存自己的工作”。?
? ? ? ? 我說過,能夠“產生一個序列”是因為我們的函數并沒有像通常意義那樣返回。return隱含的意思是函數正將執行代碼的控制權返回給函數被調用的地方。而"yield"的隱含意思是控制權的轉移是臨時和自愿的,我們的函數將來還會收回控制權。
? ? ? 在Python中,擁有這種能力的“函數”被稱為生成器,它非常的有用。生成器(以及yield語句)最初的引入是為了讓程序員可以更簡單的編寫用來產生值的序列的代碼。 以前,要實現類似隨機數生成器的東西,需要實現一個類或者一個模塊,在生成數據的同時保持對每次調用之間狀態的跟蹤。引入生成器之后,這變得非常簡單。
為了更好的理解生成器所解決的問題,讓我們來看一個例子。在了解這個例子的過程中,請始終記住我們需要解決的問題:生成值的序列。
? ? ? 生成器。一個生成器會 “生成” 值。創建一個生成器幾乎和生成器函數的原理一樣簡單。
一個生成器函數的定義很像一個普通的函數,除了當它要生成一個值的時候,使用yield關鍵字而不是return。如果一個def的主體包含yield,這個函數會自動變成一個生成器(即使它包含一個return)。除了以上內容,創建一個生成器沒有什么多余步驟了。
生成器函數返回生成器的迭代器。這可能是你最后一次見到“生成器的迭代器”這個術語了, 因為它們通常就被稱作“生成器”。要注意的是生成器就是一類特殊的迭代器。作為一個迭代器,生成器必須要定義一些方法(method),其中一個就是__next__()。如同迭代器一樣,我們可以使用next()函數來獲取下一個值。
為了從生成器獲取下一個值,我們使用next()函數,就像對付迭代器一樣。( next()會操心如何調用生成器的__next__()方法 )。
既然生成器是一個迭代器,它可以被用在for循環中。
每當生成器被調用的時候,它會返回一個值給調用者。其實 yield 就是專門給生成器使用的 return (加上點小魔法)。
下面是一個簡單的生成器函數:
>>> 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那么神奇的部分在哪里?我很高興你問了這個問題!當一個生成器函數調用yield,生成器函數的“狀態”會被凍結,所有的變量的值會被保留下來,下一行要執行的代碼的位置也會被記錄,直到再次調用next()。一旦next()再次被調用,生成器函數會從它上次離開的地方開始。如果永遠不調用next(),yield保存的狀態就被無視了。
我們來重寫get_primes()函數,這次我們把它寫作一個生成器。注意我們不再需要magical_infinite_range函數了。使用一個簡單的while循環,我們創造了自己的無窮串列。
def get_primes(number):while True:if is_prime(number):yield numbernumber += 1如果生成器函數調用了return,或者執行到函數的末尾,會出現一個StopIteration異常。 這會通知next()的調用者這個生成器沒有下一個值了(這就是普通迭代器的行為)。這也是這個while循環在我們的get_primes()函數出現的原因。如果沒有這個while,當我們第二次調用next()的時候,生成器函數會執行到函數末尾,觸發StopIteration異常。一旦生成器的值用完了,再調用next()就會出現錯誤,所以你只能將每個生成器的使用一次。下面的代碼是錯誤的:
>>> 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>>> # 然而,我們總可以再創建一個生成器 >>> # 只需再次調用生成器函數即可>>> new_generator = simple_generator_function() >>> print(next(new_generator)) # 工作正常 1因此,這個while循環是用來確保生成器函數永遠也不會執行到函數末尾的。只要調用next()這個生成器就會生成一個值。這是一個處理無窮序列的常見方法(這類生成器也是很常見的)。
?
5.1 執行流程
讓我們回到調用 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循環中對get_primes的調用,觀察一下前幾個元素是如何創建的有助于我們的理解。當for循環從get_primes請求第一個值時,我們進入get_primes,這時與進入普通函數沒有區別。
接下來,回到insolve_number_10:
這次,進入get_primes時并沒有從開頭執行,我們從第5行繼續執行,也就是上次離開的地方。
def get_primes(number):while True:if is_prime(number):yield numbernumber += 1 # <<<<<<<<<<最關鍵的是,number還保持我們上次調用yield時的值(例如3)。記住,yield會將值傳給next()的調用方,同時還會保存生成器函數的“狀態”。接下來,number加到4,回到while循環的開始處,然后繼續增加直到得到下一個素數(5)。我們再一次把number的值通過yield返回給solve_number_10的for循環。這個周期會一直執行,直到for循環結束(得到的素數大于2,000,000)。
?
更給力點
在PEP 342中加入了將值傳給生成器的支持。PEP 342加入了新的特性,能讓生成器在單一語句中實現,生成一個值(像從前一樣),接受一個值,或同時生成一個值并接受一個值。
我們用前面那個關于素數的函數來展示如何將一個值傳給生成器。這一次,我們不再簡單地生成比某個數大的素數,而是找出比某個數的等比級數大的最小素數(例如10, 我們要生成比10,100,1000,10000 ... 大的最小素數)。我們從get_primes開始:
def print_successive_primes(iterations, base=10):# 像普通函數一樣,生成器函數可以接受一個參數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的值,這個值返回給調用者的同時,將other的值也設置為那個值"。你可以通過send方法來將一個值”發送“給生成器。
def get_primes(number):while True:if is_prime(number):number = yield numbernumber += 1通過這種方式,我們可以在每次執行yield的時候為number設置不同的值。現在我們可以補齊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在發送數據給生成器的同時還返回生成器通過yield生成的值(就如同生成器中yield語句做的那樣)。
第二點,看一下prime_generator.send(None)這一行,當你用send來“啟動”一個生成器時(就是從生成器函數的第一行代碼執行到第一個yield語句的位置),你必須發送None。這不難理解,根據剛才的描述,生成器還沒有走到第一個yield語句,如果我們發生一個真實的值,這時是沒有人去“接收”它的。一旦生成器啟動了,我們就可以像上面那樣發送數據了。
?
綜述
在本系列文章的后半部分,我們將討論一些yield的高級用法及其效果。yield已經成為Python最強大的關鍵字之一?,F在我們已經對yield是如何工作的有了充分的理解,我們已經有了必要的知識,可以去了解yield的一些更“費解”的應用場景。
不管你信不信,我們其實只是揭開了yield強大能力的一角。例如,send確實如前面說的那樣工作,但是在像我們的例子這樣,只是生成簡單的序列的場景下,send幾乎從來不會被用到。下面我貼一段代碼,展示send通常的使用方式。對于這段代碼如何工作以及為何可以這樣工作,在此我并不打算多說,它將作為第二部分很不錯的熱身。
import randomdef get_data():"""返回0到9之間的3個隨機數"""return random.sample(range(10), 3)def consume():"""顯示每次傳入的整數列表的動態平均值"""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):"""產生序列集合,傳遞給消費函數(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 关键字的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Google Pixel 解锁BL、刷入
- 下一篇: Java 高级特性 --- 反射