生成器
生成器是生成一個值的特殊函式,它具有這樣的特點:第一次執行該函式時,先從頭按順序執行,在碰到yield關鍵字時該函式會暫停執行該函式後續的程式碼,並且傳回一個值;在下一次呼叫該函式執行時,程式將從上一次暫停的位置繼續往下執行。
透過一個例子來理解生成器的執行過程。求1-10的所有整數的立方並將結果列印輸出,正常使用串列的實現如下:
def lifang_ls():
"""求1-10所用整數的立方數-串列方式實現"""
ls = []
for i in range(1,11):
result = i ** 3
ls.append(result)
print(ls)
if __name__ == '__main__':
lifang_ls()
輸出結果如下:
當資料量很少時,可以很快得到結果。但是如果範圍擴大到10000甚至是100000000,就會發現程式執行時間會變長,變卡,甚至有可能會因超出記憶體空間而出現程式崩潰的現象。這是因為當資料量變得非常大的時候,記憶體需要開闢很大的空間去儲存這些資料,記憶體都被吃了,自然會變慢變卡。使用生成器就能解決這個問題。
對於上述同一個問題用生成器實現如下,將範圍擴大到1-10000000:
def lifang_generate():
"""求1-10000000所用整數的立方數-生成器方式實現"""
for i in range(1,10000001):
result = i ** 3
yield result
if __name__ == '__main__':
G = lifang_generate()
執行效果如下:
可以看到沒有任何的結果輸出,這說明程式已經可以順利執行。對於迭代器來講需要用next()方法來獲取值,修改主函式為以下情況可以列印輸出前4個整數的立方數:
if __name__ == '__main__':
G = lifang_generate()
print(next(G))
print(next(G))
print(next(G))
print(next(G))
輸出結果如下:
到此可以看到,生成器生成的值需要使用next()方法一個一個的取,它不會一次性生成所有的計算結果,只有在取值時才呼叫,這時程式會傳回計算的一個值且程式暫停;下一次取值時從上一次中斷了的地方繼續往下執行。
以取出前3個值為例,下圖為生成器程式碼解析圖:
圖解:Python直譯器從上往下解釋程式碼,首先是函式定義,這時在計算機記憶體開闢了一片空間來儲存這個函式,函式沒有被執行,繼續往下解釋;到了主函式部分,首先執行藍色箭頭1,接著往下執行到藍色箭頭2第一次呼叫生成器取值,此時生成器函式lifang_generate()開始執行,執行到生成器函式lifang_generate()的藍色箭頭2碰到yield關鍵字,這時候生成器函式暫停往下執行並且將result的結果傳回,由於是第一次執行,因此result儲存著1的立方的值,此時將1傳回,第54行程式碼print(first)將結果列印輸出。
主函式中程式接著往下執行到藍色箭頭3,生成器函式lifang_generate()第二次被呼叫,與第一次不同,第二次從上一次(也就是第一次)暫停的位置繼續往下執行,上一次停在了yield處,因此藍色箭頭3所作的事情就是執行yield後面的陳述句,也就是第48行print(‘end’),執行完成之後因for迴圈條件滿足,程式像第一次執行那樣,執行到yield處暫停並傳回一個值,此時傳回的是2的立方數,在第57行列印輸出8。
第三次呼叫(藍色箭頭4)與第二次類似,在理清了執行過程之後,程式執行結果如下:
迭代器
這裡先丟擲兩個概念:可迭代物件、迭代器。
凡是可以透過for迴圈遍歷其中的元素的物件,都是可迭代物件;之前學習得組合資料型別list(串列)、tuple(元組)、dict(字典)、集合(set)等,上一小節介紹得生成器也可以使用for迴圈來遍歷,因此,生成器也是迭代器,但迭代器不一定就是生成器,例如組合資料型別。
凡是可以透過next訪問取值得物件均為迭代器,生成器就是一種迭代器。可以看到,生成器不僅可以用for迴圈來獲取值,還可以透過next()來獲取。
Python中有一個庫collections,透過該庫的Iterable方法來判斷一個物件是否是可迭代物件;如果傳回值為True則說明該物件為可迭代的,傳回值為False則說明該物件為不可迭代。用Iterator方法來判斷一個物件是否是迭代器,根據傳回值來判斷是否為迭代器。
使用Iterable分別判斷串列,字典,字串以及一個整數型別是否是可迭代物件的程式碼如下:
from collections import Iterable
def isiterable():
"""分別判斷串列,字典,字串100,整形100是不是可迭代物件"""
ls = isinstance([],Iterable)
dic = isinstance({},Iterable)
strs = isinstance('100',Iterable)
ints = isinstance(100,Iterable)
print('輸出True表示可迭代,False表示不可迭代\n\
ls為{},dic為{},strs為{},ints為{}'.format(ls,dic,strs,ints))
def main():
isiterable()
if __name__ == '__main__':
main()
執行的輸出結果如下:
使用Iterator判斷一個物件是否是迭代器的程式碼如下,與判斷是否為可迭代物件類似:
from collections import Iterable,Iterator
def print_num():
"""定義一個產生斐波那契數列的生成器"""
a,b = 0,1
for i in range(1,10):
yield b
a,b = b,a + b
def isiterator():
"""分別判斷串列,字典、生成器是否為迭代器"""
ls_ret = isinstance([],Iterator)
dict_ret = isinstance({},Iterator)
genarate_ret = isinstance((x * 2 for i in range(10)),Iterator)
print_num_ret = isinstance(print_num(),Iterator)
print('輸出True表示該物件為迭代器,False表示該物件不是迭代器\n\
ls輸出為{},dict輸出為{},genarate輸出為{},print_num輸出為{}'.format(ls_ret,dict_ret,genarate_ret,print_num_ret))
def main():
isiterator()
if __name__ == '__main__':
main()
輸出的結果如下:
組合資料型別不是迭代器,但是屬於可迭代物件,可以透過iter()函式將其轉換位迭代器,這樣就可以使用next方法來獲取物件各個元素的值,程式碼如下:
from collections import Iterable,Iterator
def trans_to_iterator():
"""使用iter()將可迭代型別-串列轉換為迭代器"""
ls = [2,4,6,8,10]
ls_ierator = iter(ls)
ls_ierator_is = isinstance(ls_ierator,Iterator)
print('轉換後的傳回值為{},使用next取出的第一個元素的值為{}'.format(ls_ierator_is,next(ls_ierator)))
def main():
trans_to_iterator()
if __name__ == '__main__':
main()
輸出結果為:
閉包
內部函式對外部函式變數的取用,則將該函式與用到的變數稱為閉包。以下為閉包的例子:
def func(num):
print('start')
def func_in():
"""閉包內容"""
new_num = num ** 3
print(new_num)
return func_in
if __name__ == '__main__':
ret = func(10)
ret()
理解閉包是理解裝飾器的前提,同樣透過一張圖來理解閉包的執行過程:
圖解:Python直譯器從上往下解釋程式碼,首先定義一個函式,func指向了該函式(紅箭頭所示);接著到主函式執行第14行程式碼 ret = func(10),此時先執行賦值號“=”右邊的內容,這裡呼叫了函式func()並傳入10這個引數,函式func()程式碼開始執行,先是列印輸出“start”,接著定義了一個函式func_in(),func_in指向了該函式,函式沒有被呼叫,程式接著往下執行,return func_in 將函式的取用傳回,第14行程式碼用ret接收了這個傳回值,到此ret就指向了func_in所指向的函式體(綠箭頭所示)。最後執行ret所指的函式。這就是閉包的整個過程,func_in()函式以及該函式內用到的變數num就稱為閉包。
裝飾器
程式碼的編寫需要遵循封閉開放原則,封閉是指對於已有的功能程式碼實現不允許隨意進行修改,開放是指能夠對已有的功能進行擴充套件。例如一款手遊,現在已經能夠實現現有的遊戲樣式,但隨著外部環境的變化發展(市場競爭,使用者體驗等),現有的遊戲樣式已經不能滿足使用者的需求了。為了留住使用者,需要加入更多的玩法來保持使用者對該款遊戲的新鮮感,於是開發方在原來遊戲的基礎上又開發了好幾種遊戲樣式。像這樣,新的遊戲版本既增加了先的遊戲樣式,又保留了原有的遊戲樣式,體現了封閉開放的原則。 裝飾器的作用就是在不改變原來程式碼的基礎上,在原來的功能上進行拓展,保證開發的效率以及程式碼的穩定性。 列印輸出九九乘法表可以透過以下程式碼實現:
def func_1():
"""列印輸出九九乘法表"""
for i in range(1,10):
for j in range(1,i + 1):
result = i * j
print('{}*{}={}'.format(i,j,result),end=' ')
print('')
if __name__ == '__main__':
func_1()
輸出結果如下: 假如現在需要實現一個功能,在不修改func_1函式程式碼的前提下,在九九乘法表前增加一個表頭說明,在乘法表最後也增加一個說明。下麵的程式碼實現了裝飾器的功能:
def shuoming(func):
def shuoming_in():
print('以下為九九乘法表:')
func()
print('以上為九九乘法表')
return shuoming_in
def func_1():
"""列印輸出九九乘法表"""
for i in range(1,10):
for j in range(1,i + 1):
result = i * j
print('{}*{}={}'.format(i,j,result),end=' ')
print('')
if __name__ == '__main__':
func_1 = shuoming(func_1)
func_1()
輸出結果如下: 可以看到func_1函式的程式碼沒有任何修改,還實現了問題提出的要求,這其中的核心就在於最後兩行程式碼。透過下圖來理解裝飾器執行的過程:
圖解:跟之前一樣,Python直譯器自上往下解釋程式碼,遇到定義函式的程式碼不用管,因為沒有呼叫函式是不會執行的;這樣直接就來到了第22行程式碼中,程式先執行賦值號“=”右邊的程式碼,shuoming(func_1)呼叫了之前定義的函式,並傳入了func_1引數,程式轉到shuoming(func)執行,形參func接收引數func_1,此時func也指向了func_1所指向的函式(如圖中分界線上方白色方框內的藍箭頭所示);在shuoming()函式中程式碼繼續往下走,在shuoming()函式內容又定義了一個shuoming_in()函式(如圖中分界線上方白色方框內的藍色方框所示),接著往下,將shuoming_in()函式的取用傳回,至此shuoming()函式執行完畢,程式回到第22行程式碼執行,shuoming()函式的傳回值被func_1接收,此時,func_1不在指向原來的函式,轉成指向shuoming_in所指向的函式(如圖中分界線下方白色方框內的黃色箭頭)。最後呼叫func_1所指向的函式,也就是shuoming_in()函式,shuoming_in()函式內的func指向了原來func_1()所指的函式(也就是生成九九乘法表的函式),因此程式最終的結果就在九九乘法表前後各加了一個說明性字串。
以上為裝飾器的執行過程,但是以上裝飾寫法不夠簡潔,大多數情況下採取以下寫法,輸出結果是一樣的:
def shuoming(func):
def shuoming_in():
print('以下為九九乘法表:')
func()
print('以上為九九乘法表')
return shuoming_in
"""@shuoming相當於func_1 = shuoming(fucn_1)"""
@shuoming
def func_1():
"""列印輸出九九乘法表"""
for i in range(1,10):
for j in range(1,i + 1):
result = i * j
print('{}*{}={}'.format(i,j,result),end=' ')
print('')
if __name__ == '__main__':
"""直接呼叫func_1即可完成裝飾"""
func_1()
有時候有些被裝飾的函式可能有以下幾種情況:存在或不存在引數,有傳回值或沒有傳回值,引數可能定長或不定長等等,為了通用性,與爬蟲的請求程式碼一樣,裝飾器有著通用的寫法:
def tongyong(func):
def tongyong_in(*args,**kwargs):
ret = func(*args,**kwargs)
return ret
return tongyong_in
使用這個裝飾器裝飾九九乘法表一樣可以正常輸出,如果需要特定的裝飾效果,修改這個通用程式碼即可。
結束
以上為生成器、迭代器、閉包以及裝飾器的所有內容,其中裝飾器屬於難點。理解裝飾器的執行過程能夠更好的幫助我們進階學習Python。
'''
作者:Lucker_SSY
源自:https://www.cnblogs.com/ssy3340/p/9747722.html
'''