決戰Python之巔(十二)
前言
從這一章開始我們就要開始學習進階的函式知識。
函式中比較重要的就是裝飾器、迭代器、生成器這三樣,我將分開3篇介紹。
知識回顧
裝飾器
在講裝飾器之前,我們先講一點補充知識。
名稱空間
名稱到物件的對映。名稱空間是一個字典的實現,鍵為變數名,值是變數對應的值。
各個名稱空間是獨立沒有關係的,一個名稱空間中不能有重名,但是不同的名稱空間可以重名而沒有任何影響。
在我們定義了一個變數並且賦值之後,x = 1
,我們知道,1是存在記憶體地址的,那麼x和x與1的對映關係存在哪呢?誒,就是存在名稱空間的。在名稱空間中,是以字典的形式存放變數名與它的值,即{x:1234}
作用域的查詢順序
之前我們也講過,作用域的查詢順序是從最裡面一層開始找,一層一層往外面找,最後找全域性的作用域。那麼,準確的,或者學術一點解釋就是:遵循LEGB的順序。L:local,即區域性作用域;E:enclosing,即上一層作用域;G:global,即全域性作用域;B:builtins,即內建模組的作用域。
閉包
函式中我們學過高階函式,高階函式必須具備的條件是以函式作為引數傳入,或者返回一個函式。這裡我們主要看後面一個條件:返回一個函式:
def func1(msg):
def func2():
print(msg)
return func2
f = func1('helloworld')
print(f)
如果我們這裡執行func1,那麼得到的就將是func2的記憶體地址(因為func1()返回的是func2,而不是func2()):
即現在變數f就是func2的記憶體地址,也就是說 f = func2,那我們執行f(),那是不是就相當於執行了func2()?
這裡有的同學可能會問:func2()不是巢狀在func1()裡的嘛,外面怎麼可以呼叫?
真的是這樣的嘛?我們試一下…
可以看到,我們不僅成功的“執行”了func2(),甚至還用了func1()裡的變數值。
這就叫做閉包,官方解釋是:
在一些語言中,在函式中可以(巢狀)定義另一個函式時,如果內部的函式引用了外部的函式的變數,則可能產生閉包。閉包可以用來在一個函式與一組“私有”變數之間建立關聯關係。在給定函式被多次呼叫的過程中,這些私有變數能夠保持其永續性。—— 維基百科
也就是說,這裡的func2夾帶了func1的變數。你可以理解為,func1中的變數包裝在func2外面,若果你想訪問func2就先得訪問外面那層包裝。
裝飾器
前面講了那麼多,那裝飾器到底是什麼呢?
這裡我們先提供一個函式:
def print_msg():
print('Hello world')
如果有一天你想擴充套件這個函式,或者說給這個函式加點新功能,你會怎麼辦?
有的同學會說:直接改原始碼啊。
沒錯這是一個方法,但是根據開發的“擴充套件開放,修改關閉”原則,只允許你擴充套件而不能修改,怎麼辦呢?
或許你會想到高階函式,將這個函式作為一個引數傳到我需要新增功能的函式裡就行:
def login(func):
NAME = 'Kris'
PWD = '123'
name = input('輸入使用者名稱')
pwd = input('輸入密碼')
if name == NAME and pwd == PWD:
func()
else:
print('使用者名稱或密碼錯誤')
這樣你只需要執行 login(print_msg)即可。
這樣看起來可以,但你想一想,萬一哪天你寫了幾百萬條程式碼,裡面有幾百萬個地方呼叫了這個print_msg(),那你是不是得將這幾百萬個修改為login(print_msg)
。不太現實吧?
你有可能會這麼想,那我直接這樣print_msg = login(print_msg)
,這樣不就不需要改了嗎?還是print_msg(),但功能已經擴充套件了…
沒錯,但這樣又有一個新問題:
我還沒有執行print_msg()這個函式,它就自己執行login()了。Why?因為賦值操作是從右往左的,就是會先將右邊的結果算出來,再傳給左邊。這樣也不行
那怎麼辦?結合上面講的閉包,我們可以這樣做:
def login(func):
def inner():
NAME = 'Kris'
PWD = '123'
name = input('輸入使用者名稱')
pwd = input('輸入密碼')
if name == NAME and pwd == PWD:
func()
else:
print('使用者名稱或密碼錯誤')
return inner
這時候,當我們執行print_msg = login(print_msg)時,左邊的print_msg得到的僅僅是inner的記憶體地址,但並沒有執行inner裡面的程式碼。
這時候,再結合上面的知識,我們知道現在print_msg = login(print_msg)
等號左邊的print_msg
其實就相當於login()裡面的inner
,我們執行print_msg()
就相當於執行inner()
。我們可以看一下效果:
還可以簡單一點,將print_msg = login(print_msg)
這句話換成@login加在def print_msg的上面:
這個@login就是一個裝飾器。
2018.12.13補充
帶引數的裝飾器
上面我們只是簡單的實現了一個裝飾器。在實際開發中,我們開發的程式碼往往都是帶有引數的,那麼這種函式怎麼加裝飾器呢?假如我現在有下面這段程式碼:
def print_msg(msg):
print(msg)
我現在的需求還是想加一個驗證登入的功能,怎麼辦?
原裝飾器程式碼:
def login(func):
def inner():
NAME = 'Kris'
PWD = '123'
name = input('輸入使用者名稱')
pwd = input('輸入密碼')
if name == NAME and pwd == PWD:
func()
else:
print('使用者名稱或密碼錯誤')
return inner
我們來重新分析下,當我們在函式定義前加上@login,是不是就相當於執行了xx= login(xx)
(xx是@login下面定義的函式名),而這句程式碼最終返回的是inner函式
,即xx = inner
,之前的操作是接下來直接呼叫inner函式inner()
,也就是xx()
,即xx()= inner()
。但是現在,我讓xx=print_msg,那是否就意味著print_msg = inner
,進而print_msg(msg)= inner(msg)
,所以我們只需要在定義inner的時候,給inner加一個變數即可:
新裝飾器程式碼:
def login(func):
def inner(arg): # 新增一個arg變數
NAME = 'Kris'
PWD = '123'
name = input('輸入使用者名稱')
pwd = input('輸入密碼')
if name == NAME and pwd == PWD:
func(arg)
else:
print('使用者名稱或密碼錯誤')
return inner
這樣就能解決需要裝飾的函式帶引數的問題。…然而,事情沒那麼簡單,不知你是否發現,現在的這個裝飾器只能裝飾一種型別的函式即只有一個引數的函式,但實際生活中我們往往需要它可以裝飾不限量的引數的函式,這可怎麼辦呢?
不知你是否還記得,我們將函式引數的時候講過一種特殊的引數,非固定引數,*args,**kwargs,這裡我們就可以用到它倆:
改進後裝飾器程式碼:
def login(func):
def inner(*arg,**kwargs): # 新增一個arg變數
NAME = 'Kris'
PWD = '123'
name = input('輸入使用者名稱')
pwd = input('輸入密碼')
if name == NAME and pwd == PWD:
func(*arg,**kwargs) #注意這裡也需要使用非固定引數,否則會變成位置引數
else:
print('使用者名稱或密碼錯誤')
return inner
這樣你裝飾不帶引數的函式或者多個引數的函式就都沒問題~
可以思考下為何func(*args,**kwargs)必須要使用非固定引數~
在作死的道路上繼續前行…
來來來,都別走,接下來還有。我們已經實現了帶引數的裝飾器,那麼我現在又有一個新的需求(別打我- -要打別打臉,畢竟靠臉吃飯),我現在需要增加login的功能,就是可以讓使用者選擇登入模式,比如QQ/WeiXin/WeiBo這三種,平常寫這種可能很容易實現:
def login_with_type(type):
if type == 'QQ':
#QQ登入程式碼
elif type == 'WeiXin':
#WeiXin登入程式碼
else:
#WeiBo登入程式碼
那麼現在我需要加在裝飾器裡,怎麼操作呢?
有的童鞋可能說:我在寫個裝飾器唄,再加一層裝飾唄…這個我們日後再說。我現在想直接改login的程式碼,怎麼實現呢?
定義login再加一個引數?def login(type,func)
?不行,因為這樣定義就無法使用@語句,
@login(‘QQ’,print_msg)這是個錯誤的語法,Python 3里根本就沒有~那可怎麼辦呢?我們又不能直接給login傳type,type傳進去是個字串,到最後執行字串()肯定會報錯。
好了…我直接說吧,這裡再加一層,看程式碼:
二改後裝飾器程式碼:
def login(type):
def outer(func):
def inner(*arg,**kwargs):
if type == 'QQ':
#QQ登入程式碼
elif type == 'WeiXin':
#WeiXin登入程式碼
else:
#WeiBo登入程式碼
name = input('輸入使用者名稱')
pwd = input('輸入密碼')
if name == NAME and pwd == PWD:
func(*arg,**kwargs)
else:
print('使用者名稱或密碼錯誤')
return inner
return outer
現在我們再裝飾函式的時候,可以直接在函式定義的上面寫:@login(‘QQ’)。
我來分析下具體過程:@login(‘QQ’)首先執行的是login(‘QQ’),返回的是outer函式,注意不是呼叫,即login('QQ') = outer
,這時候再執行@語法,也就是@outer,很熟悉吧,這樣又回到了最開始最簡單的裝飾器,此時outer自動把下面定義的函式名作為引數傳進去,開始後面的事情。
永攀作死的高峰…
現在我們來討論下,如果,一個函式被多個裝飾器裝飾,是什麼執行順序呢?
def dec_one(func):
print('1111')
def inner_one():
print('aaaa')
func() #三
return inner_one
def dec_two(func):
print('2222')
def inner_two():
print('bbbb')
func() #四
return inner_two
@dec_one
@dec_two
def print_msg(): #二
print('test')
print_msg() #一
…最終列印的順序是啥?
首先我們要將一個原則,就是自下而上的原則,即當被多個裝飾器裝飾的時候,離它最近的先上,然後由近到遠依次裝飾。
那麼,這裡一處的print_msg
函式,就相當於dec_one(dec_two(print_msg)) #這裡的print_msg是二處的
,執行dec_one(dec_two(print_msg))()
,首先執行dec_two(print_msg)
,進入dec_two
,先列印2222
,然後返回inner_two
函式,即dec_two(print_msg) = inner_two
,然後執行dec_one(inner_two)
,先列印1111
,然後返回inner_one
,這裡注意dec_one
在最外層,所以到這裡dec_one(dec_two(print_msg))()
就變成inner_one()
,而inner_one()
內三處的func
變成了inner_two
,所以,執行inner_one()
,先列印aaaa
,然後執行inner_two()
,列印bbbb
,四處的func是定義的print_msg
,最後列印一句test
。
所以最終結果是:2222 1111 aaaa bbbb test
沒錯吧~今天搞了一天才明白的- -不容易啊…
還是不懂的童鞋,可以敲一敲程式碼,然後每句程式碼前都打個斷點,一步一步看執行順序。