第五篇、Python之叠代器與生成器
1、叠代和遞歸等概念
循環(loop):指的是在滿足條件的情況下,重復執行同一段代碼。比如,while語句,for循環。
叠代(iterate):指的是按照某種順序逐個訪問列表中的每一項。比如,for語句。Python中,叠代永遠是取出元素本身,而非元素的索引。對於有序集合,元素確實是有索引的。使用 enumerate() 函數獲得索引。
遞歸(recursion):指的是一個函數不斷調用自身的行為。比如,以編程方式輸出著名的斐波納契數列。
遍歷(traversal):指的是按照一定的規則訪問樹形結構中的每個節點,而且每個節點都只訪問一次。
2、叠代器協議
1) 叠代器協議是指:對象必須提供一個next方法
2) 可叠代對象:實現了叠代器協議的對象(如何實現:對象內部定義一個__iter__()方法)
3) 協議是一種約定,可叠代對象實現了叠代器協議,python的內部工具(如for循環,sum,min,max函數等)使用叠代器協議訪問對象。
註:可以通過__next__取值,就是叠代器(遵循叠代器協議生成的都是可叠代對象,叠代器就是可叠代對象)
l=["ye","ba","er","sun"] inter_l=l.__iter__() print(inter_l.__next__()) print(next(inter_l))
3、python中強大的for循環機制
for循環的本質:循環所有對象,全都是使用叠代器協議。
for循環的原理:基於叠代器協議提供了一個統一的可以遍歷所有對象的方法,即在遍歷之前,先調用對象的__iter__方法將其轉換成一個叠代器,然後使用叠代器協議去實現循環訪問,這樣所有的對象就都可以通過for循環來遍歷了。
列表,字符串,元組,字典,集合,文件對象等本質上來說都不是可叠代對象,在使用for循環的時候內部是先調用他們內部的_iter_方法,使他們變成了可叠代對象,然後在使用可叠代對象的_next_方法依次循環元素,當元素循環完時,會觸發StopIteration異常,for循環會捕捉到這種異常,終止叠代。
訪問方式常見的有下標方式訪問、叠代器協議訪問、for循環訪問
l=[‘a‘,‘b‘,‘c‘] #一:下標訪問方式 print(l[0]) print(l[1]) print(l[2]) # print(l[3])#超出邊界報錯:IndexError #二:遵循叠代器協議訪問方式 diedai_l=l.__iter__() print(diedai_l.__next__()) print(diedai_l.__next__()) print(diedai_l.__next__()) # print(diedai_l.__next__())#超出邊界報錯:StopIteration #三:for循環訪問方式 #for循環l本質就是遵循叠代器協議的訪問方式,先調用diedai_l=l.__iter__()方法,或者直接diedai_l=iter(l),然後依次執行diedai_l.next(),直到for循環捕捉到StopIteration終止循環 #for循環所有對象的本質都是一樣的原理 for i in l:#diedai_l=l.__iter__() print(i) #i=diedai_l.next() #四:用while去模擬for循環做的事情 diedai_l=l.__iter__() while True: try: print(diedai_l.__next__()) except StopIteration: print(‘叠代完畢了,循環終止了‘) break訪問方式
4. for循環的作用
對於序列類型的對象可使用下標的訪問方式,但是對於非序列類型(字典,集合,文件對象等),for循環提供了訪問遍歷機制。
l=[1,2,3] index=0 while index < len(l): print(l[index]) index+=1View Code
while 需要加異常處理,for默認都已經內置。
文件默認就是叠代器:
f=open(‘a.txt‘,‘r‘) f.__next__ f.__iter__ print(f) print(f.__iter__()) for line in f: #f.__iter__() print(line) i=f.__iter__() while True: try: print(next(i)) except StopIteration: breakView Code
5.生成器初識
生成器的本質:
可以理解為一種數據類型,這種數據類型自動實現了叠代器協議(其他的數據類型需要調用自己內置的__iter__方法),所以生成器就是可叠代對象。
生成器分類及在python中的表現形式:(Python有兩種不同的方式提供生成器)
1)生成器函數:常規函數定義,但是,使用yield語句而不是return語句返回結果。yield語句一次返回一個結果,在每個結果中間,掛起函數的狀態,以便下次重它離開的地方繼續執行
2)生成器表達式:類似於列表推導,但是,生成器返回按需產生結果的一個對象,而不是一次構建一個結果列表
生成器的優點:
Python使用生成器對延遲操作提供了支持。所謂延遲操作,是指在需要的時候才產生結果,而不是立即產生結果。這也是生成器的主要好處。
生成器小結:
1)是可叠代對象
2)現了延遲計算,省內存啊
3)生成器本質和其他的數據類型一樣,都是實現了叠代器協議,只不過生成器附加了一個延遲計算省內存的好處,其余的可叠代對象可沒有這點好處!!!
可叠代對象:只要對象本身有__iter__方法,那它就是可叠代的。 # 只有內置了iter方法就是可叠代的對象。
d={‘a‘:1,‘b‘:2,‘c‘:3}
d.__iter__() # iter(d)
執行對象下的__iter__方法,得到的結果就是叠代器 i=d.__iter__()
d={‘a‘:1,‘b‘:2,‘c‘:3} i=iter(d) while True: try: print(next(i)) except StopIteration: # StopIteration 不能叫做錯誤,是一個結束信號。 break l=[‘a‘,‘b‘,‘c‘,‘d‘,‘e‘,‘f‘] #列表也可以使用叠代器 i=iter(l) #i=l.__iter__() while True: try: print(next(i)) except StopIteration: break d={‘a‘:1,‘b‘:2,‘c‘:3} d.__iter__ for k in d: #d.__iter__() # for 循環 print(k) s={1,2,3,4} for i in s: print(i)View Code
為什麽要用叠代器:
優點:
- 叠代器提供了一種不依賴於索引的取值方式,這樣就可以遍歷那些沒有索引的可叠代對象了(字典,集合,文件)
- 叠代器與列表比較,叠代器是惰性計算的,更節省內存
缺點:
- 無法獲取叠代器的長度,使用不如列表索引取值靈活
- 一次性的,只能往後取值,不能倒著取值
from collections import Iterable,Iterator s=‘hello‘ l=[1,2,3] t=(1,2,3) d={‘a‘:1} set1={1,2,3,4} f=open(‘a.txt‘) s.__iter__() l.__iter__() t.__iter__() d.__iter__() set1.__iter__() f.__iter__() # 都是可叠代的 print(isinstance(s,Iterable)) #True print(isinstance(l,Iterable)) #True print(isinstance(t,Iterable)) #True print(isinstance(d,Iterable)) #True print(isinstance(set1,Iterable)) #True print(isinstance(f,Iterable)) #True # 查看是否是叠代器 print(isinstance(s,Iterator)) #False print(isinstance(l,Iterator)) #False print(isinstance(t,Iterator)) #False print(isinstance(d,Iterator)) #False print(isinstance(set1,Iterator)) #False print(isinstance(f,Iterator)) #True
6、 生成器函數
e.send與next(e)的區別:
- 如果函數內yield是表達式形式,那麽必須先next(e)
- 二者的共同之處是都可以讓函數在上次暫停的位置繼續運行,不一樣的地方在於send在觸發下一次代碼的執行時,會順便給yield傳一個值。
def lay_eggs(num): egg_list=[] for egg in range(num): egg_list.append(‘蛋%s‘ %egg) return egg_list yikuangdan=lay_eggs(10) #我們拿到的是蛋 print(yikuangdan) def lay_eggs(num): for egg in range(num): res=‘蛋%s‘ %egg yield res print(‘下完一個蛋‘) laomuji=lay_eggs(10)#我們拿到的是一只母雞 print(laomuji) print(laomuji.__next__()) print(laomuji.__next__()) print(laomuji.__next__()) egg_l=list(laomuji) print(egg_l) #演示只能往後不能往前 #演示蛋下完了,母雞就死了下蛋
生成器與return有何區別?
return只能返回一次函數就徹底結束了,而yield能返回多次值。函數在暫停以及繼續下一次運行時的狀態是由yield保存。
yield把函數變成生成器-->叠代器
from collections import Iterator #生成器就是一個函數,這個函數內包含有yield這個關鍵字 def test(): print(‘one‘) yield 1 #return 1 print(‘two‘) yield 2 #return 2 print(‘three‘) yield 3 #return 2 print(‘four‘) yield 4 #return 2 print(‘five‘) yield 5 #return 2 g=test() print(g) print(isinstance(g,Iterator)) g.__iter__() # g.__next__() # res=next(g) # print(res) # # res=next(g) # print(res) # # res=next(g) # print(res) # # res=next(g) # print(res) for i in g: print(i)yield
next觸發函數的運行。函數變成叠代器,有執行效果,同時可以向外拉值。next一下函數執行一下。
def countdown(n): print(‘start coutdown‘) while n > 0: yield n #1 n-=1 print(‘done‘) g=countdown(5) # print(g) # print(next(g)) # print(next(g)) # print(next(g)) # print(next(g)) # print(next(g)) # print(next(g)) # for i in g: #iter(g) # print(i) # while True: # try: # print(next(g)) # except StopIteration: # break # # def func(): # n=0 # while True: # yield n # n+=1 # # f=func() # print(next(f))next
import time def tail(file_path): with open(file_path,‘r‘) as f: f.seek(0,2) while True: line=f.readline() if not line: time.sleep(0.3) continue else: # print(line) yield line tail(‘/tmp/a.txt‘)View Code
#加顏色 g=tail(‘/tmp/a.txt‘) for line in g: if ‘error‘ in line: print(‘\033[45m%s\033[0m‘ %line)View Code
#/usr/bin/env python import time #定義階段:定義倆生成器函數 def tail(file_path): with open(file_path,‘r‘) as f: f.seek(0,2) while True: line=f.readline() if not line: time.sleep(0.3) # print(‘====>‘) continue else: #print(line,end=‘‘) yield line def grep(pattern,lines): for line in lines: if pattern in line: yield line #調用階段:得到倆生成器對象 g1=tail(‘/tmp/a.txt‘) g2=grep(‘error‘,g1) #next觸發執行g2生成器函數 for i in g2: print(i)View Code
#如果在一個函數內部yield的使用方式是表達式形式的話,如x=yield,那麽該函數成為協程函數 def eater(name): print(‘%s start to eat food‘ %name) food_list=[] while True: food=yield food_list print(‘%s get %s ,to start eat‘ %(name,food)) food_list.append(food) print(‘done‘) e=eater(‘鋼蛋‘) # print(e) print(next(e)) print(e.send(‘包子‘)) print(e.send(‘韭菜餡包子‘)) print(e.send(‘大蒜包子‘)) #為什麽叫協程? #協程怎麽用?協程函數
7、 生成器表達式(三元表達式)和列表解析
name=‘alex‘ name=‘linhaifeng‘ res=‘SB‘ if name == ‘alex‘ else ‘shuai‘ print(res)
egg_list=[‘雞蛋%s‘ %i for i in range(10)] #列表解析 laomuji=(‘雞蛋%s‘ %i for i in range(10)) #生成器表達式 print(laomuji) print(next(laomuji)) #next本質就是調用__next__ print(laomuji.__next__()) print(next(laomuji))View Code
總結:
1.把列表解析的[]換成()得到的就是生成器表達式
2.列表解析與生成器表達式都是一種便利的編程方式,只不過生成器表達式更節省內存
3.Python不但使用叠代器協議,讓for循環變得更加通用。大部分內置函數,也是使用叠代器協議訪問對象的。例如, sum函數是Python的內置函數,該函數使用叠代器協議訪問對象,而生成器實現了叠代器協議,所以,我們可以直接這樣計算一系列值的和:
sum(x ** 2 for x in xrange(4))
而不用多此一舉的先構造一個列表:
sum([x ** 2 for x in xrange(4)])
8、生成器總結
綜上已經對生成器有了一定的認識,下面我們以生成器函數為例進行總結
- 語法上和函數類似:生成器函數和常規函數幾乎是一樣的。它們都是使用def語句進行定義,差別在於,生成器使用yield語句返回一個值,而常規函數使用return語句返回一個值
- 自動實現叠代器協議:對於生成器,Python會自動實現叠代器協議,以便應用到叠代背景中(如for循環,sum函數)。由於生成器自動實現了叠代器協議,所以,我們可以調用它的next方法,並且,在沒有值可以返回的時候,生成器自動產生StopIteration異常
- 狀態掛起:生成器使用yield語句返回一個值。yield語句掛起該生成器函數的狀態,保留足夠的信息,以便之後從它離開的地方繼續執行
優點一:生成器的好處是延遲計算,一次返回一個結果。也就是說,它不會一次生成所有的結果,這對於大數據量處理,將會非常有用。
sum([i for i in range(100000000)]) #列表解析:內存占用大,機器容易卡死 sum(i for i in range(100000000)) #生成器表達式:幾乎不占內存View Code
優點二:生成器還能有效提高代碼可讀性
def index_words(text): result = [] if text: result.append(0) for index, letter in enumerate(text, 1): if letter == ‘ ‘: result.append(index) return result print(index_words(‘hello alex da sb‘))View Code
def index_words(text): if text: yield 0 for index, letter in enumerate(text, 1): if letter == ‘ ‘: yield index g=index_words(‘hello alex da sb‘) print(g) print(g.__next__()) print(g.__next__()) print(g.__next__()) print(g.__next__()) print(g.__next__())#報錯View Code
這裏,至少有兩個充分的理由說明 ,使用生成器比不使用生成器代碼更加清晰:
- 使用生成器以後,代碼行數更少。大家要記住,如果想把代碼寫的Pythonic,在保證代碼可讀性的前提下,代碼行數越少越好
- 不使用生成器的時候,對於每次結果,我們首先看到的是result.append(index),其次,才是index。也就是說,我們每次看到的是一個列表的append操作,只是append的是我們想要的結果。使用生成器的時候,直接yield index,少了列表append操作的幹擾,我們一眼就能夠看出,代碼是要返回index。
合理使用生成器,能夠有效提高代碼可讀性。只要大家完全接受了生成器的概念,理解了yield語句和return語句一樣,也是返回一個值。那麽,就能夠理解為什麽使用生成器比不使用生成器要好,能夠理解使用生成器真的可以讓代碼變得清晰易懂。生成器只(叠代)遍歷一次,如需要重復讀取,需要保存數據,再次獲取。
人口信息.txt文件內容 {‘name‘:‘北京‘,‘population‘:10} {‘name‘:‘南京‘,‘population‘:100000} {‘name‘:‘山東‘,‘population‘:10000} {‘name‘:‘山西‘,‘population‘:19999} def get_provice_population(filename): with open(filename) as f: for line in f: p=eval(line) #提取出來的line都是字符串形式,需要使用eval提取起數據結構 yield p[‘population‘] gen=get_provice_population(‘人口信息.txt‘) all_population=sum(gen) for p in gen: print(p/all_population) 執行上面這段代碼,將不會有任何輸出,這是因為,生成器只能遍歷一次。在我們執行sum語句的時候,就遍歷了我們的生成器,當我們再次遍歷我們的生成器的時候,將不會有任何記錄。所以,上面的代碼不會有任何輸出。 因此,生成器的唯一註意事項就是:生成器只能遍歷一次。eval
def test(): for i in range(4): yield i g=test() g1=(i for i in g) g2=(i for i in g1) print(list(g1)) print(list(g2))note1
def add(n,i): return n+i def test(): for i in range(4): yield i g=test() for n in [1,10]: g=(add(n,i) for i in g) print(list(g))note2
import os def init(func): def wrapper(*args,**kwargs): g=func(*args,**kwargs) next(g) return g return wrapper @init def list_files(target): while 1: dir_to_search=yield for top_dir,dir,files in os.walk(dir_to_search): for file in files: target.send(os.path.join(top_dir,file)) @init def opener(target): while 1: file=yield fn=open(file) target.send((file,fn)) @init def cat(target): while 1: file,fn=yield for line in fn: target.send((file,line)) @init def grep(pattern,target): while 1: file,line=yield if pattern in line: target.send(file) @init def printer(): while 1: file=yield if file: print(file) g=list_files(opener(cat(grep(‘python‘,printer())))) g.send(‘/test1‘)協程應用:grep -rl /dir
【參考文檔】
python基礎之叠代器協議和生成器:https://www.cnblogs.com/luchuangao/p/6685626.html
叠代器協議和生成器:https://www.cnblogs.com/chenice/articles/6135714.html
第五篇、Python之叠代器與生成器