函數(shù)進階知識
函數(shù)名只是一個指向函數(shù)的變量
在python中,一切皆對象。函數(shù)名只是一個指向函數(shù)的變量,為了驗證這一點,我們可以查看修改和刪除函數(shù)名對程序的影響:
def someFunc(): # someFunc只是一個變量名,它指向了我們創(chuàng)建的函數(shù)
print("Now in a function")
if __name__ == '__main__':
anotherFunc = someFunc # 用另一個變量名指向剛創(chuàng)建的函數(shù)
anotherFunc() # 輸出Now in a function
# 刪除了一個指向函數(shù)的變量名,對函數(shù)本身并沒有影響;
# 因為還有一個變量anotherFunc指向函數(shù),因此函數(shù)本體并不會被回收
del someFunc
anotherFunc() # 還是輸出Now in a function
可以看到,在python中函數(shù)名和一個普通變量一樣,只不過它指向了一組語句。
嵌套函數(shù)和變量作用域
在函數(shù)內(nèi)是可以定義另一個函數(shù)的,這叫做嵌套函數(shù)。作用域是程序運行時變量可被訪問的范圍,定義在函數(shù)內(nèi)的變量是局部變量,局部變量的作用范圍只能是函數(shù)內(nèi)部范圍內(nèi),它不能在函數(shù)外引用。根據(jù)這個原理,對于嵌套函數(shù)來說,在內(nèi)層的函數(shù)可以訪問外層函數(shù)定義的變量。
如下例所示:
def nested_func():
msg = "This is a message"
# 在函數(shù)內(nèi)部定義一個函數(shù),打印消息
def print_message():
# 在嵌套函數(shù)內(nèi)層可以訪問外層函數(shù)的變量
print(msg)
# 調(diào)用打印消息的函數(shù)
print_message() # output: This is a message
if __name__ == '__main__':
nested_func()
將函數(shù)作為返回值
函數(shù)由于是一個對象,它是可以作為另一個函數(shù)的返回值或者作為參數(shù)傳遞給其他函數(shù)。
def nested_func(msg):
# 在函數(shù)內(nèi)部定義一個函數(shù),打印消息
def print_message():
# 在嵌套函數(shù)內(nèi)層可以訪問外層函數(shù)的變量
print(msg)
return print_message
if __name__ == '__main__':
msg = "This is a message"
aFunction = nested_func(msg) # 將aFunction指向了nested_Func(msg)的返回值,即print_message
aFunction() # 等同于調(diào)用了print_message()
這里將返回的函數(shù)賦值給另一個函數(shù),這樣可以實現(xiàn)對內(nèi)部函數(shù)從外部進行傳值。
閉包(Closure)
什么是閉包
兩個嵌套的函數(shù),內(nèi)部函數(shù)使用外部函數(shù)的變量。這套變量+內(nèi)部函數(shù)整體,就叫做閉包。
閉包避免了使用全局變量,此外,閉包允許將函數(shù)與其所操作的某些數(shù)據(jù)(環(huán)境)關(guān)連起來。這一點與面向?qū)ο缶幊淌欠浅n愃频?,在面對象編程中,對象允許我們將某些數(shù)據(jù)(對象的屬性)與一個或者多個方法相關(guān)聯(lián)。
為什么需要閉包
剛才提到過,閉包可以實現(xiàn)對內(nèi)部函數(shù)從外部進行傳值。實現(xiàn)這種功能,有兩套方案:使用類和使用閉包。使用閉包的方案我們前面已經(jīng)介紹過,我們下面看看如何用類實現(xiàn)這個功能:
class someClass(object):
def __call__(self, msg):
print(msg)
if __name__ == '__main__':
msg = "This is a message"
aFunction = someClass()
aFunction(msg) # 輸出 This is a message
這里就實現(xiàn)了和上面用嵌套函數(shù)一樣的功能,在這里這個aFunction雖然是一個實例對象,但是我們定義了__call__方法之后,它也可以像函數(shù)一樣被調(diào)用。
這種實現(xiàn)方式的缺陷在于,python3的類默認(rèn)會繼承object(Python2 略有不同,繼承了object的叫做新式類,沒有繼承的叫做經(jīng)典類),它里面自帶了一系列我們用不到的方法,會占用相對較大的空間。
print(dir(aFunction))
# 輸出結(jié)果:
# ['__call__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
而閉包則更加精簡,占用的空間更小。
裝飾器的內(nèi)部執(zhí)行原理
什么是裝飾器
裝飾器就是一種閉包,只是這個閉包的函數(shù)名等于原先的函數(shù)名,從而改變了原先函數(shù)的指向。
def my_decorator(func):
def print_msg():
print("new message has been added!")
func()
return print_msg
def plain_print_msg():
print("entering plain print msg")
if __name__ == '__main__':
# plain_print_msg作為實參被傳給my_decorator
# aFunc指向my_decorator的返回值,也就是print_msg
aFunc = my_decorator(plain_print_msg)
# 相當(dāng)于在指向print_msg,其內(nèi)部的func = plain_print_msg
aFunc()
print("-" * 10)
# 將原先的函數(shù)名plain_print_msg直接指向my_decorator的返回值
# 可以看到和上面部分的代碼實現(xiàn)的功能是相同的
plain_print_msg = my_decorator(plain_print_msg)
plain_print_msg()
可以看到,執(zhí)行下面兩行代碼之后,函數(shù)名稱并沒有變, 但是卻指向了一個新的函數(shù),在新的函數(shù)內(nèi),除了執(zhí)行原函數(shù)之外,還加了一些其他的功能,這就是裝飾器的實現(xiàn)原理。
在實際使用裝飾器時,我們并不需要手動修改函數(shù)名的指向,而是可以由@運算符來進行代勞,上面的代碼可以改寫如下:
def my_decorator(func):
def print_msg():
print("new message has been added!")
func()
return print_msg
@my_decorator
def plain_print_msg():
print("entering plain print msg")
if __name__ == '__main__':
plain_print_msg()
這里的@my_decorator和我們上面手動實現(xiàn)的代碼功能是一樣的。
為什么需要使用裝飾器
那么在開發(fā)過程中進行功能擴充時,為什么不直接修改函數(shù)源碼,而要使用裝飾器呢?
開放封閉原則:已經(jīng)實現(xiàn)的功能代碼不允許修改,但是可以被擴展。也即已經(jīng)實現(xiàn)的功能代碼塊封閉,而對擴展開發(fā)開放。這是因為在比較大的系統(tǒng)中,可能有很多模塊使用同一個函數(shù),如果為了修改功能,直接對函數(shù)進行修改,那么其他地方對函數(shù)的引用可能會出錯。而使用了裝飾器之后,可以在裝飾器內(nèi)梳理代碼邏輯,這樣不該動其他地方的代碼也可以正常加上新功能。
裝飾器什么時候開始生效的
裝飾器的生效不會等到函數(shù)進行調(diào)用,而是在解釋器讀到裝飾符開始,就會對函數(shù)進行裝飾。為了說明這一點,我們可以看下面這一段代碼:
def my_decorator(func):
print("entering my decorator!")
def print_msg():
print("new message has been added!")
func()
return print_msg
@my_decorator
def plain_print_msg():
print("entering plain print msg")
if __name__ == '__main__':
pass # 注意這里并沒有執(zhí)行函數(shù)調(diào)用
輸出結(jié)果:
entering my decorator!
可以看到函數(shù)并沒有被調(diào)用,但是裝飾器已經(jīng)被加裝在函數(shù)之上了。
對帶參數(shù)的函數(shù)進行裝飾
通過上面對函數(shù)的學(xué)習(xí),我們已經(jīng)知道裝飾器的工作原理了,因此如果需要對帶參數(shù)的函數(shù)進行裝飾,無非就是在閉包的內(nèi)層函數(shù)上加上相應(yīng)的參數(shù)。示例如下:
import time
def time_wrapper(func):
def timer(cnt):
start_time = time.time()
func(cnt)
end_time = time.time()
print("time consumed by %s: %s second" % (func.__name__, str(end_time - start_time)))
return timer
@time_wrapper
def count1(cnt):
lst = []
for i in range(cnt):
lst.insert(0, i)
@time_wrapper
def count2(cnt):
lst = []
for i in range(cnt):
lst.append(i)
if __name__ == '__main__':
count1(10000)
count2(10000)
對有返回值的函數(shù)進行裝飾
在了解了裝飾器的原理之后,我們就應(yīng)該知道如果原函數(shù)帶有返回值的話,我們需要在裝飾器的什么地方進行返回了:
def my_decorator(func):
print("entering my decorator!")
def print_msg():
print("new message has been added!")
return func() # 將函數(shù)的返回值作為返回值
return print_msg
@my_decorator
def plain_print_msg():
print("entering plain print msg")
return "OK" # 函數(shù)帶有返回值,指示打印狀態(tài)
if __name__ == '__main__':
# 這里給的實際上是print_msg()的返回值,也就是func()
# 形參func = plain_print_msg,因此返回值和原函數(shù)plain_print_msg的返回值是相同的
ret = plain_print_msg()
print(ret)
多個裝飾器裝飾同一個函數(shù)
當(dāng)有多個裝飾器裝飾同一個函數(shù)時,最靠近函數(shù)的(位于程序最下端的)裝飾器最先裝飾,然后依次由近及遠進行裝飾。而調(diào)用閉包時,由遠及近進行調(diào)用。
def decorator1(func):
print("entering decorator1")
def add_msg():
print("using decorator1: add_msg")
func()
return add_msg
def decorator2(func):
print("entering decorator2")
def add_another_msg():
print("using decorator2: add_another_msg")
return add_another_msg
@decorator1 # 距離函數(shù)較遠的裝飾器后裝飾,先調(diào)用(包在外層)
@decorator2 # 距離函數(shù)較近的裝飾器先裝飾,后調(diào)用(包在內(nèi)層)
def plain_print_msg():
print("entering plain print msg")
return "OK" # 函數(shù)帶有返回值,指示打印狀態(tài)
if __name__ == '__main__':
plain_print_msg()
輸出的結(jié)果:
entering decorator2
entering decorator1
using decorator1: add_msg
using decorator2: add_another_msg
用類實現(xiàn)裝飾器
明白了裝飾器的實現(xiàn)原理之后,就可以知道用類也是可以實現(xiàn)裝飾器的。這就是我們之前講的用類實現(xiàn)閉包功能的@裝飾符版本。
class Test():
def __init__(self, func):
self._func = func
def __call__(self, *args, **kwargs):
print("----調(diào)用裝飾器----")
self._func(*args, **kwargs)
@Test
def my_fun(a, b, c):
print(a, b, c)
if __name__ == '__main__':
my_fun(10, 20, 'main')
輸出結(jié)果:
----調(diào)用裝飾器----
10 20 main
帶參數(shù)的裝飾器
當(dāng)裝飾器帶有參數(shù)時,調(diào)用過程分為兩步:
- 調(diào)用裝飾器函數(shù)(閉包外層的函數(shù))并將參數(shù)作為實參傳遞
- 用上一步的返回值當(dāng)作裝飾器對函數(shù)進行裝飾
因此需要給裝飾器加上參數(shù)時,我們要對閉包進行再一層包裝,在最外層傳遞入裝飾器中傳進來的參數(shù)。
一個對操作進行權(quán)限檢查的例子如下:
def get_level(level):
def level_check_wrapper(func):
def level_check():
print("正在檢查權(quán)限:")
if level == 1:
print("權(quán)限等級為1")
return func()
elif level == 2:
print("權(quán)限等級為2")
return func()
return level_check
return level_check_wrapper
@get_level(1)
def low_level_op():
print("低級權(quán)限操作")
print([i for i in range(5)])
@get_level(2)
def high_level_op():
print("高級權(quán)限操作")
print([item for item in "abcdefgh"])
if __name__ == '__main__':
low_level_op()
high_level_op()