1. 程式人生 > >決戰Python之巔(十二)

決戰Python之巔(十二)

前言

從這一章開始我們就要開始學習進階的函式知識。
函式中比較重要的就是裝飾器、迭代器、生成器這三樣,我將分開3篇介紹。

知識回顧

裝飾器

在講裝飾器之前,我們先講一點補充知識。

名稱空間

名稱到物件的對映。名稱空間是一個字典的實現,鍵為變數名,值是變數對應的值。
各個名稱空間是獨立沒有關係的,一個名稱空間中不能有重名,但是不同的名稱空間可以重名而沒有任何影響。

在我們定義了一個變數並且賦值之後,x = 1,我們知道,1是存在記憶體地址的,那麼x和x與1的對映關係存在哪呢?誒,就是存在名稱空間的。在名稱空間中,是以字典的形式存放變數名與它的值,即{x:1234}

(假設1的記憶體地址是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
在這裡插入圖片描述
沒錯吧~今天搞了一天才明白的- -不容易啊…
還是不懂的童鞋,可以敲一敲程式碼,然後每句程式碼前都打個斷點,一步一步看執行順序。