一 裝飾器介紹
1.1 為何要用裝飾器
軟件的設計應該遵循開放封閉原則,即對擴展是開放的,而對修改是封閉的。對擴展開放,意味著有新的需求或變化時,可以對現有代碼進行擴展,以適應新的情況。對修改封閉,意味著對象一旦設計完成,就可以獨立完成其工作,而不要對其進行修改。
軟件包含的所有功能的源代碼以及調用方式,都應該避免修改,否則一旦改錯,則極有可能產生連鎖反應,最終導致程序崩潰,而對于上線后的軟件,新需求或者變化又層出不窮,我們必須為程序提供擴展的可能性,這就用到了裝飾器。
1.2 什么是裝飾器
’裝飾’代指為被裝飾對象添加新的功能,’器’代指器具/工具,裝飾器與被裝飾的對象均可以是任意可調用對象。概括地講,裝飾器的作用就是在不修改被裝飾對象源代碼和調用方式的前提下為被裝飾對象添加額外的功能。裝飾器經常用于有切面需求的場景,比如:插入日志、性能測試、事務處理、緩存、權限校驗等應用場景,裝飾器是解決這類問題的絕佳設計,有了裝飾器,就可以抽離出大量與函數功能本身無關的雷同代碼并繼續重用。
提示:可調用對象有函數,方法或者類,此處我們單以本章主題函數為例,來介紹函數裝飾器,并且被裝飾的對象也是函數。
二 裝飾器的實現
函數裝飾器分為:無參裝飾器和有參裝飾兩種,二者的實現原理一樣,都是’函數嵌套+閉包+函數對象’的組合使用的產物。
2.1 無參裝飾器的實現
如果想為下述函數添加統計其執行時間的功能
import time
def index():
? ? time.sleep(3)
? ? print('Welcome to the index page’)
? ? return 200
index() #函數執行
遵循不修改被裝飾對象源代碼的原則,我們想到的解決方法可能是這樣
start_time=time.time()
index() #函數執行
stop_time=time.time()
print('run time is %s' %(stop_time-start_time))
考慮到還有可能要統計其他函數的執行時間,于是我們將其做成一個單獨的工具,函數體需要外部傳入被裝飾的函數從而進行調用,我們可以使用參數的形式傳入
def wrapper(func): # 通過參數接收外部的值
? ? start_time=time.time()
? ? res=func()
? ? stop_time=time.time()
? ? print('run time is %s' %(stop_time-start_time))
? ? return res
但之后函數的調用方式都需要統一改成
wrapper(index)
wrapper(其他函數)
這便違反了不能修改被裝飾對象調用方式的原則,于是我們換一種為函數體傳值的方式,即將值包給函數,如下
def timer(func):
? ? def wrapper(): # 引用外部作用域的變量func
? ? ? ? start_time=time.time()
? ? ? ? res=func()
? ? ? ? stop_time=time.time()
? ? ? ? print('run time is %s' %(stop_time-start_time))
? ? ? ? return res
? ? return wrapper
這樣我們便可以在不修改被裝飾函數源代碼和調用方式的前提下為其加上統計時間的功能,只不過需要事先執行一次timer將被裝飾的函數傳入,返回一個閉包函數wrapper重新賦值給變量名 /函數名index,如下
index=timer(index)#得到index=wrapper,wrapper攜帶對外作用域的引用:func=原始的index()# 執行的是wrapper(),在wrapper的函數體內再執行最原始的index
至此我們便實現了一個無參裝飾器timer,可以在不修改被裝飾對象index源代碼和調用方式的前提下為其加上新功能。但我們忽略了若被裝飾的函數是一個有參函數,便會拋出異常。
def home(name):
? ? time.sleep(5)
? ? print('Welcome to the home page',name)
home=timer(home)
home('湯勇')
#拋出異常
TypeError: wrapper() takes 0 positional arguments but 1 was given
之所以會拋出異常,是因為home(‘湯勇’)調用的其實是wrapper(‘湯勇’),而函數wrapper沒有參數。wrapper函數接收的參數其實是給最原始的func用的,為了能滿足被裝飾函數參數的所有情況,便用上args+*kwargs組合(,于是修正裝飾器timer如下
def timer(func):
? ? def wrapper(*args,**kwargs):
? ? ? ? start_time=time.time()
? ? ? ? res=func(*args,**kwargs)
? ? ? ? stop_time=time.time()
? ? ? ? print('run time is %s' %(stop_time-start_time))
? ? ? ? return res
? ? return wrapper
此時我們就可以用timer來裝飾帶參數或不帶參數的函數了,但是為了簡潔而優雅地使用裝飾器,Python提供了專門的裝飾器語法來取代index=timer(index)的形式,需要在被裝飾對象的正上方單獨一行添加@timer,當解釋器解釋到@timer時就會調用timer函數,且把它正下方的函數名當做實參傳入,然后將返回的結果重新賦值給原函數名
@timer # index=timer(index)
def index():
? ? time.sleep(3)
? ? print('Welcome to the index page')
? ? return 200
@timer # index=timer(home)? ? ? ? ? def home(name):
? ? time.sleep(5)
? ? print('Welcome to the home page’,name)
如果我們有多個裝飾器,可以疊加多個
@deco3
@deco2
@deco1
def index():
? ? pass
疊加多個裝飾器也無特殊之處,上述代碼語義如下:
index=deco3(deco2(deco1(index)))
2.2 有參裝飾器的實現
了解無參裝飾器的實現原理后,我們可以再實現一個用來為被裝飾對象添加認證功能的裝飾器,實現的基本形式如下
def deco(func):
? ? def wrapper(*args,**kwargs):
? ? ? ? 編寫基于文件的認證,認證通過則執行res=func(*args,**kwargs),并返回res
? ? return wrapper
如果我們想提供多種不同的認證方式以供選擇,單從wrapper函數的實現角度改寫如下
def deco(func):
? ? ? ? def wrapper(*args,**kwargs):
? ? ? ? ? ? if driver == 'file':
? ? ? ? ? ? ? ? 編寫基于文件的認證,認證通過則執行res=func(*args,**kwargs),并返回res
? ? ? ? ? ? elif driver == 'mysql':
? ? ? ? ? ? ? ? 編寫基于mysql認證,認證通過則執行res=func(*args,**kwargs),并返回res
? ? ? ? return wrapper
函數wrapper需要一個driver參數,而函數deco與wrapper的參數都有其特定的功能,不能用來接受其他類別的參數,可以在deco的外部再包一層函數auth,用來專門接受額外的參數,這樣便保證了在auth函數內無論多少層都可以引用到
def auth(driver):
? ? def deco(func):
? ? ? ? ……
? ? return deco
此時我們就實現了一個有參裝飾器,使用方式如下
先調用auth_type(driver='file'),得到@deco,deco是一個閉包函數,
包含了對外部作用域名字driver的引用,@deco的語法意義與無參裝飾器一樣
@auth(driver='file')
def index():? ?
? ? pass
@auth(driver='mysql')
def home():
? ? pass?
可以使用help(函數名)來查看函數的文檔注釋,本質就是查看函數的doc屬性,但對于被裝飾之后的函數,查看文檔注釋
@timer
def home(name):
? ? '''
? ? home page function
? ? :param name: str
? ? :return: None
? ? '''
? ? time.sleep(5)
? ? print('Welcome to the home page',name)
print(help(home))
'''
打印結果:
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
None
在被裝飾之后home=wrapper,查看home.name也可以發現home的函數名確實是wrapper,想要保留原函數的文檔和函數名屬性,需要修正裝飾器
def timer(func):
? ? def wrapper(*args,**kwargs):
? ? ? ? start_time=time.time()
? ? ? ? res=func(*args,**kwargs)
? ? ? ? stop_time=time.time()
? ? ? ? print('run time is %s' %(stop_time-start_time))
? ? ? ? return res
? ? wrapper.__doc__=func.__doc__
? ? wrapper.__name__=func.__name__
? ? return wrapper
按照上述方式來實現保留原函數屬性過于麻煩,functools模塊下提供一個裝飾器wraps專門用來幫我們實現這件事,用法如下
from functools import wraps
def timer(func):
? ? @wraps(func)
? ? def wrapper(*args,**kwargs):
? ? ? ? start_time=time.time()
? ? ? ? res=func(*args,**kwargs)
? ? ? ? stop_time=time.time()
? ? ? ? print('run time is %s' %(stop_time-start_time))
? ? ? ? return res
? ? return wrapper
三 迭代器介紹
迭代器即用來迭代取值的工具,而迭代是重復反饋過程的活動,其目的通常是為了逼近所需的目標或結果,每一次對過程的重復稱為一次“迭代”,而每一次迭代得到的結果會作為下一次迭代的初始值,單純的重復并不是迭代
while True:
? ? msg = input('>>: ').strip()
? ? print(msg)
下述while循環才是一個迭代過程,不僅滿足重復,而且以每次重新賦值后的index值作為下一次循環中新的索引進行取值,反復迭代,最終可以取盡列表中的值
goods=['mac','lenovo','acer','dell','sony']
index=0
while index < len(goods):
? ? print(goods[index])
? ? index+=1
3.1 可迭代對象
通過索引的方式進行迭代取值,實現簡單,但僅適用于序列類型:字符串,列表,元組。對于沒有索引的字典、集合等非序列類型,必須找到一種不依賴索引來進行迭代取值的方式,這就用到了迭代器。
要想了解迭代器為何物,必須事先搞清楚一個很重要的概念:可迭代對象(Iterable)。從語法形式上講,內置有__iter__方法的對象都是可迭代對象,字符串、列表、元組、字典、集合、打開的文件都是可迭代對象:
{'name':'egon'}.__iter__
{7,8,9}.__iter__
……
3.2 迭代器對象
調用obj.iter()方法返回的結果就是一個迭代器對象(Iterator)。迭代器對象是內置有iter和next方法的對象,打開的文件本身就是一個迭代器對象,執行迭代器對象.iter()方法得到的仍然是迭代器本身,而執行迭代器.next()方法就會計算出迭代器中的下一個值。 迭代器是Python提供的一種統一的、不依賴于索引的迭代取值方式,只要存在多個“值”,無論序列類型還是非序列類型都可以按照迭代器的方式取值
>>> s={1,2,3} # 可迭代對象s
>>> i=iter(s)? # 本質就是在調用s.__iter__(),返回s的迭代器對象i,
>>> next(i) # 本質就是在調用i.__next__()
1
>>> next(i)
2
>>> next(i)
3
>>> next(i)? #拋出StopIteration的異常,代表無值可取,迭代結束
四 for循環原理
有了迭代器后,我們便可以不依賴索引迭代取值了,使用while循環的實現方式如下
goods=['mac','lenovo','acer','dell','sony']
i=iter(goods) #每次都需要重新獲取一個迭代器對象
while True:
? ? try:
? ? ? ? print(next(i))
? ? except StopIteration: #捕捉異常終止循環
? ? ? ? break
for循環又稱為迭代循環,in后可以跟任意可迭代對象,上述while循環可以簡寫為
goods=['mac','lenovo','acer','dell','sony']
for item in goods:?
? ? print(item)
for 循環在工作時,首先會調用可迭代對象goods內置的iter方法拿到一個迭代器對象,然后再調用該迭代器對象的next方法將取到的值賦給item,執行循環體完成一次循環,周而復始,直到捕捉StopIteration異常,結束迭代。
五 迭代器的優缺點
基于索引的迭代取值,所有迭代的狀態都保存在了索引中,而基于迭代器實現迭代的方式不再需要索引,所有迭代的狀態就保存在迭代器中,然而這種處理方式優點與缺點并存:
5.1 優點:
1、為序列和非序列類型提供了一種統一的迭代取值方式。
2、惰性計算:迭代器對象表示的是一個數據流,可以只在需要時才去調用next來計算出一個值,就迭代器本身來說,同一時刻在內存中只有一個值,因而可以存放無限大的數據流,而對于其他容器類型,如列表,需要把所有的元素都存放于內存中,受內存大小的限制,可以存放的值的個數是有限的。
5.2 缺點:
1、除非取盡,否則無法獲取迭代器的長度
2、只能取下一個值,不能回到開始,更像是‘一次性的’,迭代器產生后的唯一目標就是重復執行next方法直到值取盡,否則就會停留在某個位置,等待下一次調用next;若是要再次迭代同個對象,你只能重新調用iter方法去創建一個新的迭代器對象,如果有兩個或者多個循環使用同一個迭代器,必然只會有一個循環能取到值。
六? 生成器與yield
若函數體包含yield關鍵字,再調用函數,并不會執行函數體代碼,得到的返回值即生成器對象
>>> def my_range(start,stop,step=1):
...? ? print('start...')
...? ? while start < stop:
...? ? ? ? yield start
...? ? ? ? start+=step
...? ? print('end...')
...
>>> g=my_range(0,3)
>>> g
<generator object my_range at 0x104105678>
生成器內置有__iter__和__next__方法,所以生成器本身就是一個迭代器
>>> g.__iter__
<method-wrapper '__iter__' of generator object at 0x1037d2af0>
>>> g.__next__
<method-wrapper '__next__' of generator object at 0x1037d2af0>
因而我們可以用next(生成器)觸發生成器所對應函數的執行
>>> next(g) # 觸發函數執行直到遇到yield則停止,將yield后的值返回,并在當前位置掛起函數
start...
0
>>> next(g) # 再次調用next(g),函數從上次暫停的位置繼續執行,直到重新遇到yield...
1
>>> next(g) # 周而復始...
2
>>> next(g) # 觸發函數執行沒有遇到yield則無值返回,即取值完畢拋出異常結束迭代
end...
Traceback (most recent call last):
? File "<stdin>", line 1, in <module>
StopIteration
既然生成器對象屬于迭代器,那么必然可以使用for循環迭代,如下:
>>> for i in countdown(3):
...? ? print(i)
...
countdown start
3
2
1
Done!
有了yield關鍵字,我們就有了一種自定義迭代器的實現方式。yield可以用于返回值,但不同于return,函數一旦遇到return就結束了,而yield可以保存函數的運行狀態掛起函數,用來返回多次值
七? yield表達式應用
在函數內可以采用表達式形式的yield
>>> def eater():
...? ? print('Ready to eat')
...? ? while True:
...? ? ? ? food=yield
...? ? ? ? print('get the food: %s, and start to eat' %food)
...
可以拿到函數的生成器對象持續為函數體send值,如下
>>> g=eater() # 得到生成器對象
>>> g
<generator object eater at 0x101b6e2b0>
>>> next(e) # 需要事先”初始化”一次,讓函數掛起在food=yield,等待調用g.send()方法為其傳值
Ready to eat
>>> g.send('包子')
get the food: 包子, and start to eat
>>> g.send('雞腿')
get the food: 雞腿, and start to eat
針對表達式形式的yield,生成器對象必須事先被初始化一次,讓函數掛起在food=yield的位置,等待調用g.send()方法為函數體傳值,g.send(None)等同于next(g)。
? 我們可以編寫裝飾器來完成為所有表達式形式yield對應生成器的初始化操作,如下
def init(func):
? ? def wrapper(*args,**kwargs):
? ? ? ? g=func(*args,**kwargs)
? ? ? ? next(g)
? ? ? ? return g
? ? return wrapper
@init
def eater():
? ? print('Ready to eat')
? ? while True:
? ? ? ? food=yield
? ? ? ? print('get the food: %s, and start to eat' %food)
表達式形式的yield也可以用于返回多次值,即變量名=yield 值的形式,如下
>>> def eater():
...? ? print('Ready to eat')
...? ? food_list=[]
...? ? while True:
...? ? ? ? food=yield food_list
...? ? ? ? food_list.append(food)
...
>>> e=eater()
>>> next(e)
Ready to eat
[]
>>> e.send('蒸羊羔')
['蒸羊羔']
>>> e.send('蒸熊掌')
['蒸羊羔', '蒸熊掌']
>>> e.send('蒸鹿尾兒')
['蒸羊羔', '蒸熊掌', '蒸鹿尾兒']
八 三元表達式、列表生成式、生成器表達式
8.1 三元表達式
三元表達式是python為我們提供的一種簡化代碼的解決方案,語法如下
res=條件成立時返回的值if條件else條件不成立時返回的值
針對下述場景
def max2(x,y):
? ? if x > y:
? ? ? ? return x
? ? else:
? ? ? ? return y
res = max2(1,2)
用三元表達式可以一行解決
x=1
y=2
res = x if x > y else y # 三元表達式
8.2 列表生成式
列表生成式是python為我們提供的一種簡化代碼的解決方案,用來快速生成列表,語法如下
[expression for item1 in iterable1 if condition1
for item2 in iterable2 if condition2
...
for itemN in iterableN if conditionN
]
#類似于
res=[]
for item1 in iterable1:
? ? if condition1:
? ? ? ? for item2 in iterable2:
? ? ? ? ? ? if condition2
? ? ? ? ? ? ? ? ...
? ? ? ? ? ? ? ? for itemN in iterableN:
? ? ? ? ? ? ? ? ? ? if conditionN:
? ? ? ? ? ? ? ? ? ? ? ? res.append(expression)
針對下述場景
egg_list=[]
for i in range(10):
? ? egg_list.append('雞蛋%s' %i)
用列表生成式可以一行解決
egg_list=['雞蛋%s'%iforiinrange(10)]
8.3 生成器表達式
創建一個生成器對象有兩種方式,一種是調用帶yield關鍵字的函數,另一種就是生成器表達式,與列表生成式的語法格式相同,只需要將[]換成(),即:
(expression for item in iterable if condition)
對比列表生成式返回的是一個列表,生成器表達式返回的是一個生成器對象
>>> [x*x for x in range(3)]
[0, 1, 4]
>>> g=(x*x for x in range(3))
>>> g
<generator object <genexpr> at 0x101be0ba0>
對比列表生成式,生成器表達式的優點自然是節省內存(一次只產生一個值在內存中)
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g) #拋出異常StopIteration
如果我們要讀取一個大文件的字節數,應該基于生成器表達式的方式完成
with open('db.txt','rb') as f:
? ? nums=(len(line) for line in f)
? ? total_size=sum(nums) # 依次執行next(nums),然后累加到一起得到結果