【博學谷學習記錄】超強總結,用心分享|Java基礎分享-程序間通訊方式
在Python中,裝飾器和迭代器、生成器都是非常重要的高階函式。
在講裝飾器之前,我們先要學習以下三個內容:
一、函式的作用域
1、作用域介紹
Python中的作用域分為四種情況:
- L:local,區域性作用域,即函式中定義的變數;
- E:enclosing,巢狀的父級函式的區域性作用域,即包含此函式的上級函式的區域性作用域,但不是全域性的;
- G:globa,全域性變數,就是模組級別定義的變數;
- B:built-in,系統固定模組裡面的變數,比如int, bytearray等。
搜尋變數的優先順序順序依次是:L –> E –> G –>B,即:區域性作用域>外層作用域>當前模組中的全域性>Python內建作用域。
1 x = int(2.9) # int built-in 2 3 g_count = 0 # global 4 5 def outer(): 6 o_count = 1 # enclosing 7 def inner(): 8 i_count = 2 # local 9 print(o_count) 10 print(i_count) # 找不到 11 inner() 12 outer() 13 14 print(o_count) #找不到
當然,local和enclosing是相對的,enclosing變數相對上層來說也是local。
2、作用域的產生
在Python中,只有函式(def、lambda)、類(class)以及模組(module)才會引入新的作用域,其它的程式碼塊(如if、try、for等)是不會引入新的作用域的,如下程式碼:
1 if 2>1: 2 x = 1 3 print(x) # 1
if並沒有引入一個新的作用域,x仍處在當前作用域中,後面程式碼可以使用。
1 def test(): 2 x = 2 3 print(x) # NameError: name 'x2' is not defined
上面這段程式碼則會報錯,因為def、class、lambda是可以引入新作用域的。
3、變數的修改
1 x = 6 2 def f(): 3 print(x) 4 x = 5 5 f() 6 7 # 錯誤的原因在於print(x)時,直譯器會在區域性作用域找,會找到x=5(函式已經載入到記憶體),但x使用在宣告前了,所以報錯: 8 # local variable 'x' referenced before assignment.如何證明找到了x=5呢?簡單:註釋掉x=5,x=6,報錯為:name 'x' is not defined 9 10 # 同理 11 x = 6 12 def f(): 13 x += 1 # local variable 'x' referenced before assignment. 14 f()
4、global關鍵字
當內部作用域想修改外部作用域的變數時,就要用到global和nonlocal關鍵字了,當修改的變數是在全域性作用域(global作用域)上的,就要使用global先宣告一下,程式碼如下:
1 count = 10 2 def outer(): 3 global count 4 print(count) #10 5 count = 100 6 print(count) #100 7 outer() 8 print(count) #100
5、nonlocal關鍵字
global關鍵字宣告的變數必須在全域性作用域上,不能巢狀作用域上,當要修改巢狀作用域(enclosing作用域,外層非全域性作用域)中的變數怎麼辦呢,這時就需要nonlocal關鍵字了,程式碼如下:
1 def outer(): 2 count = 10 3 def inner(): 4 nonlocal count 5 count = 20 6 print(count) #20 7 inner() 8 print(count) #20 9 outer()
6、作用域小結
- 變數查詢順序:LEGB,作用域區域性>外層作用域>當前模組中的全域性>python內建作用域;
- 只有函式、類以及模組才能引入新的作用域;
- 對於一個變數,內部作用域先宣告就會覆蓋外部變數,不宣告直接使用,就會使用外部作用域的變數;
- 內部作用域要修改外部作用域變數的值時,全域性變數要使用global關鍵字,巢狀作用域變數要使用nonlocal關鍵字。nonlocal是python3新增的關鍵字,有了這個 關鍵字,就能完美的實現閉包了;
二、函式即物件
在Python中,函式和之前學過的字串、整型、列表等一樣都是物件,而且函式是最高階的物件(物件是類的例項化,可以呼叫相應的方法,函式是包含變數的物件)。如下:
1 def foo(): 2 print('i am the foo') 3 bar() 4 5 def bar(): 6 print('i am the bar') 7 8 foo()
接著,我們再聊一下函式在記憶體的儲存情況:
函式物件的呼叫僅僅比其它物件多了一個()而已!foo,bar與a,b一樣都是個變數名。
既然函式是物件,那麼自然滿足下面兩個條件:
1、函式可以被賦值給其他變數
1 def foo(): 2 print('foo') 3 bar=foo 4 bar() 5 foo() 6 print(id(foo),id(bar)) #1386464801520 1386464801520
2、函式可以被定義在另外一個函式內(作為引數或者返回值),類似於整型、字串等物件
1 # *******函式名作為引數********** 2 def foo(func): 3 print('foo') 4 func() 5 6 def bar(): 7 print('bar') 8 9 foo(bar) 10 11 # *******函式名作為返回值********* 12 def foo(): 13 print('foo') 14 return bar 15 16 def bar(): 17 print('bar') 18 19 b = foo() 20 b()
三、函式的閉包
如下一個函式:
1 def foo(): 2 x = 1
3 def bar(): 4 print(x) 5 return bar
我們想要呼叫bar函式,有什麼辦法呢?,如下:
1 # 方法一 2 foo()() 3 4 # 方法二 5 func = foo() 6 func()
那麼以上兩種呼叫方式,有什麼區別嗎?
貌似沒什麼區別,但是有一個疑問:函式foo已經呼叫執行完畢了,再呼叫bar函式時,為什麼沒有報錯(直接呼叫bar函式時卻報錯了)?
因為:函式foo return的bar函式是一個閉包函式,有x這個環境變數。
1、閉包函式
定義:如果在一個函式裡,對在外部作用域(但不是在全域性作用域)的變數進行引用,那麼內部函式就被認為是一個閉包函式;
如上例項,bar就是內部函式,bar裡引用了外部作用域的變數x(x在外部作用域foo裡面,不是全域性作用域),則這個內部函式bar就是一個閉包函式;
再稍微講究一點的解釋是,閉包=函式塊+定義函式時的環境,bar就是函式塊,x就是環境,當然這個環境可以有很多,不止一個簡單的x;
2、用途
用途一:
1 # 用途1:當閉包執行完後,仍然能夠保持住當前的執行環境。 2 # 比如說,如果你希望函式的每次執行結果,都是基於這個函式上次的執行結果。我以一個類似棋盤遊戲的例子 3 # 來說明。假設棋盤大小為50*50,左上角為座標系原點(0,0),我需要一個函式,接收2個引數,分別為方向 4 # (direction),步長(step),該函式控制棋子的運動。棋子運動的新的座標除了依賴於方向和步長以外, 5 # 當然還要根據原來所處的座標點,用閉包就可以保持住這個棋子原來所處的座標。 6 7 origin = [0, 0] # 座標系統原點 8 legal_x = [0, 50] # x軸方向的合法座標 9 legal_y = [0, 50] # y軸方向的合法座標 10 def create(pos=origin): 11 def player(direction,step): 12 # 這裡應該首先判斷引數direction,step的合法性,比如direction不能斜著走,step不能為負等 13 # 然後還要對新生成的x,y座標的合法性進行判斷處理,這裡主要是想介紹閉包,就不詳細寫了。 14 new_x = pos[0] + direction[0]*step 15 new_y = pos[1] + direction[1]*step 16 pos[0] = new_x 17 pos[1] = new_y 18 #注意!此處不能寫成 pos = [new_x, new_y],原因在上文有說過 19 return pos 20 return player 21 22 player = create() # 建立棋子player,起點為原點 23 print (player([1,0],10)) # 向x軸正方向移動10步 24 print (player([0,1],20)) # 向y軸正方向移動20步 25 print (player([-1,0],10)) # 向x軸負方向移動10步
用途二:
1 # 用途2:閉包可以根據外部作用域的區域性變數來得到不同的結果,這有點像一種類似配置功能的作用,我們可以 2 # 修改外部的變數,閉包根據這個變數展現出不同的功能。比如有時我們需要對某些檔案的特殊行進行分析,先 3 # 要提取出這些特殊行。 4 5 def make_filter(keep): 6 def the_filter(file_name): 7 file = open(file_name) 8 lines = file.readlines() 9 file.close() 10 filter_doc = [i for i in lines if keep in i] 11 return filter_doc 12 return the_filter 13 14 # 如果我們需要取得檔案"result.txt"中含有"pass"關鍵字的行,則可以這樣使用例子程式 15 filter = make_filter("pass") 16 filter_result = filter("result.txt")
四、裝飾器
裝飾器本質上是一個函式,該函式用來處理其他函式,它可以讓其他函式在不需要修改程式碼的前提下增加額外的功能,裝飾器的返回值也是一個函式物件。它經常用於有切面需求的場景,比如:插入日誌、效能測試、事務處理、快取、許可權校驗等應用場景。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量與函式功能本身無關的雷同程式碼並繼續重用。概括的講,裝飾器的作用就是為已經存在的物件新增額外的功能。
有如下一個函式:
1 def timer(): 2 print('timer')
我們要測試這個函式的執行時間,該怎麼做?
1 import time 2 def timer(): 3 start_time = time.time() 4 print('timer') 5 time.sleep(1.22) 6 end_time = time.time() 7 print('執行時間為:%s'%(end_time-start_time)) 8 9 timer() #1.230109453201294
如果有成百上千個函式需要測試的話,這樣做工作量就太大了,那麼可以這樣做:
1 import time 2 def timer(func): 3 start_time = time.time() 4 func() 5 end_time = time.time() 6 print('執行時間為:%s'%(end_time-start_time)) 7 8 def bar(): 9 time.sleep(1.23) 10 print('bar') 11 12 timer(bar) #1.2442445755004883
這麼做貌似沒什麼問題了,但是我們發現函式的呼叫發生了變化,之前我們呼叫bar函式只要bar()就行了,現在則要用timer(bar)來呼叫。如果很多程式碼已經是寫好了的,那麼我們還要去修改原始碼,顯然,這樣的方法是不可取的。那麼,還有什麼更好的方法呢?就要正式用到裝飾器了,如下:
1 import time 2 def timer(func): 3 def deco(): 4 start_time = time.time() 5 func() 6 end_time = time.time() 7 print('執行時間為:%s'%(end_time-start_time)) 8 return deco 9 10 def bar(): 11 time.sleep(1.23) 12 print('bar') 13 14 bar = timer(bar) 15 bar()
函式timer就是裝飾器,它把真正的業務方法func包裹在函式裡面,看起來像bar被上下時間函式裝飾了。在這個例子中,函式進入和退出時 ,被稱為一個橫切面(Aspect),這種程式設計方式被稱為面向切面的程式設計(Aspect-Oriented Programming)。
1、簡單的裝飾器
上面這段程式碼基本實現了裝飾器的功能,但和普通函式呼叫比起來,還多了一行:bar = timer(bar),我們可以在需要呼叫的函式前面加上@timer來替代這一行,如下:
1 import time 2 def timer(func): 3 def deco(): 4 start_time = time.time() 5 func() 6 end_time = time.time() 7 print('執行時間為:%s'%(end_time-start_time)) 8 return deco 9 10 @timer # bar = timer(bar) 11 def bar(): 12 time.sleep(1.23) 13 print('bar') 14 15 @timer # foo = timer(foo) 16 def foo(): 17 time.sleep(2.23) 18 print('foo') 19 20 bar() 21 foo()
如上所示,這樣我們就可以省去bar = timer(bar)這一句了,直接呼叫bar()即可得到想要的結果。如果我們有其他的類似函式,我們可以繼續呼叫裝飾器來修飾函式,而不用重複修改函式或者增加新的封裝。這樣,我們就提高了程式的可重複利用性,並增加了程式的可讀性。
這裡需要注意的問題:foo = timer(foo)其實是把deco引用的物件引用給了foo,而deco裡的變數func之所以可以用,就是因為deco是一個閉包函式。
@timer幫我們做的事情就是當我們執行業務邏輯bar()時,執行的程式碼由紅框部分轉到藍框部分,僅此而已。
裝飾器在Python使用如此方便都要歸因於Python的函式能像普通的物件一樣能作為引數傳遞給其他函式,可以被賦值給其他變數,可以作為返回值,可以被定義在另外一個函式內。
2、帶引數的被裝飾函式
1 import time 2 def timer(func): 3 def deco(a,b): 4 start_time = time.time() 5 func(a,b) 6 end_time = time.time() 7 print('執行時間為:%s'%(end_time-start_time)) 8 return deco 9 10 @timer # bar = timer(bar) 11 def bar(a,b): 12 time.sleep(1.23) 13 print(a+b) 14 15 @timer # foo = timer(foo) 16 def foo(a,b): 17 time.sleep(2.23) 18 print(a-b) 19 20 bar(1,2) 21 foo(3,4)
3、不定長引數
1 import time 2 def timer(func): 3 def deco(*args,**kwargs): 4 start_time = time.time() 5 func(*args,**kwargs) 6 end_time = time.time() 7 print('執行時間為:%s'%(end_time-start_time)) 8 return deco 9 10 @timer # bar = timer(bar) 11 def add(*args,**kwargs): 12 time.sleep(1.23) 13 sum = 0 14 for i in args: 15 sum += i 16 print(sum) 17 18 add(1,2,3,4,5,6,7,8,9,10) #55
4、帶引數的裝飾器
裝飾器還有更大的靈活性,例如帶引數的裝飾器:在上面的裝飾器呼叫中,比如@timer,該裝飾器唯一的引數就是執行業務的函式。裝飾器的語法允許我們在呼叫時,提供其它引數,比如@decorator(a)。這樣,就為裝飾器的編寫和使用提供了更大的靈活性。
1 import time 2 def time_logger(flag=0): 3 def timer(func): 4 def deco(*args,**kwargs): 5 start_time = time.time() 6 func(*args,**kwargs) 7 end_time = time.time() 8 print('執行時間為:%s'%(end_time-start_time)) 9 if flag: 10 print('將這個函式的執行時間記錄到日誌當中') 11 return deco 12 return timer 13 14 @time_logger(3) 15 def add(*args,**kwargs): 16 time.sleep(1.23) 17 sum = 0 18 for i in args: 19 sum += i 20 print(sum) 21 22 add(1,2,3,4,5,6,7,8,9,10)
@time_logger(3) 做了兩件事:
(1)time_logger(3):得到閉包函式timer,裡面儲存環境變數flag
(2)@timer:add=timer(add)
上面的time_logger是允許帶引數的裝飾器。它實際上是對原有裝飾器的一個函式封裝,並返回一個裝飾器(一個含有引數的閉包函式)。當我 們使用@time_logger(3)呼叫的時候,Python能夠發現這一層的封裝,並把引數傳遞到裝飾器的環境中。
我們可以通過裝飾器time_logger中的引數flag的值來控制是否將函式的執行時間寫入日誌,比如:flag=0就不寫入,flag!=0就寫入。
5、類裝飾器
相比函式裝飾器,類裝飾器具有靈活度大、高內聚、封裝性等優點。使用類裝飾器還可以依靠類內部的__call__方法,當使用 @ 形式將裝飾器附加到函式上時,就會呼叫此方法。
1 import time 2 3 class Foo(object): 4 def __init__(self,func): 5 self._func = func 6 7 def __call__(self): 8 start_time = time.time() 9 self._func() 10 end_time = time.time() 11 print('函式執行時間為:%s' % (end_time - start_time)) 12 13 @Foo 14 def bar(): 15 time.sleep(1.25) 16 print('bar') 17 18 bar()
可以看到,類裝飾器沒有巢狀關係了,直接使用類當中的__call__方法。
6、裝飾器例項
用裝飾器,寫一個例項,判斷使用者是否登陸,邏輯是:執行程式,列印選單,選擇要進入的選單,如果未登入,則要進行登入,如果是已登入則直接展示選單。
1 import os 2 def login(): 3 ''' 4 登入函式,登入成功的話將username寫入user檔案當中 5 ''' 6 username = input('username:') 7 passwd = input('passwd:') 8 if username == 'admin' and passwd == '123456': 9 with open('user','a+',encoding='utf-8') as f: 10 f.write(username) 11 print('登入成功') 12 else: 13 print('使用者名稱或密碼錯誤') 14 15 def auth(func): 16 ''' 17 校驗是否登入的裝飾器 18 ''' 19 def check(*args,**kwargs): 20 if os.path.exists('user'): # 判斷user檔案是否存在 21 func(*args,**kwargs) # 假設user檔案存在就代表登入成功,執行函式 22 else: 23 print('您還未登入') 24 login() # 不存在則呼叫登入函式 25 func(*args, **kwargs) # 登入成功後再執行函式 26 return check 27 28 @auth 29 def home(): 30 print('Welcome to Home Page!!') 31 32 @auth 33 def finance(): 34 print('Welcome to Finance Page!!') 35 36 @auth 37 def add(): 38 print('Welcome to AddProduct Page!!') 39 40 def menu(): # 列印選單函式 41 msg = ''' 42 1:首頁 43 2:金融 44 3:新增商品 45 ''' 46 print(msg) 47 m = { 48 "1":home, 49 "2":finance, 50 "3":add 51 } 52 choice = input('請輸入您的選擇:').strip() 53 if choice in m: 54 m[choice]() # 呼叫對應的函式 55 else: 56 print('輸入錯誤') 57 menu() 58 59 60 if __name__ == '__main__': 61 menu()