在之前的文章中寫了Python的生成器和迭代器,今天給大家分享一下關於裝飾器的一些知識。
閉包
在講裝飾器之前一定要提及的就是閉包,因爲Python中的閉包是實現裝飾器的基礎。
# 定義一個函數
def test(number):
# 在函數內部再定義一個函數,並且這個函數用到了外邊函數的變量,那麼將這個函數以及用到的一些變量稱之爲閉包
def test_in(number_in):
print("in test_in 函數, number_in is %d" % number_in)
return number+number_in
# 其實這裏返回的就是閉包的結果,也就是返回test_in這個內部函數的引用
return test_in
# 給test函數賦值,這個20就是給參數number
# 這裏的ret實際上接收的是運行完test之後的test_in的引用
ret = test(20)
# 注意這裏的100其實給參數number_in,ret(100)在這裏等價於test_in(100)
print(ret(100))
#注 意這裏的200其實給參數number_in
print(ret(200))
運行結果:
in test_in 函數, number_in is 100
120
in test_in 函數, number_in is 200
220
從上面的例子我們可以得出一些結論:
閉包= 內部函數 + 自由變量
閉包的3個特點:
1 函數的嵌套定義
2 外部函數返回內部函數的引用
3 內部函數可以使用外部函數提供的自由變量
閉包的另一個實例:
def line_conf(a, b):
def line(x):
return a*x + b
return line
#注意這裏的line1和line2是兩個獨立運行的空間互不影響
line1 = line_conf(1, 1)
line2 = line_conf(4, 5)
print(line1(5))
print(line2(5))
這個例子中,函數line與變量a,b構成閉包。在創建閉包的時候,我們通過line_conf的參數a,b說明了這兩個變量的取值,這樣,我們就確定了函數的最終形式(y = x + 1和y = 4x + 5)。我們只需要變換參數a,b,就可以獲得不同的直線表達函數。由此,我們可以看到,閉包也具有提高代碼可複用性的作用。
如果沒有閉包,我們需要每次創建直線函數的時候同時說明a,b,x。這樣,我們就需要更多的參數傳遞,也減少了代碼的可移植性。
注意點:
由於閉包引用了外部函數的局部變量,則外部函數的局部變量沒有及時釋放,消耗內存。
修改外部函數的變量
#python3的方法
def counter(start=0):
def incr():
nonlocal start
start += 1
return start
return incr
c1 = counter(5)
print(c1())
#python2 的方法 python2的方法實際並沒有修改start的值,只是利用了bug.
def counter(start=0):
count = [start]
def incr():
count[0] += 1
return count[0]
return incr
c1 = counter(5)
print(c1())
閉包的總結:
函數名只是函數代碼空間的引用,當函數名賦值給一個對象的時候 就是引用傳遞
閉包就是一個嵌套定義的函數,在外層運行時纔開始內層函數的定義,然後將內部函數的引用傳遞函數外的對象
內部函數和使用的外部函數提供的變量構成的整體稱爲閉包
裝飾器
在初步瞭解完畢閉包之後,我們就可以來看看裝飾器到底是什麼樣的了。
裝飾器是程序開發中經常會用到的一個功能,用好了裝飾器,開發效率如虎添翼,所以這也是Python面試中必問的問題。
裝飾器相對於其他是一個比較難明白的一個知識點,但是這是開發必備的基礎,也是作爲一個python程序員必須掌握的。
#### 1 ####
def foo():
print('foo')
foo # 表示是函數
foo() # 表示執行foo函數
#### 2 ####
def foo():
print('foo')
foo = lambda x: x + 1
foo() # 執行的是lambda表達式,而不再是原來的foo函數,因爲foo這個名字被重新指向了另外一個匿名函數,也就是foo成爲了這個匿名函數的引用
裝飾器究竟是爲了實現什麼功能而被開發出來的呢?
寫代碼要遵循開放封閉原則,雖然在這個原則是用的面向對象開發,但是也適用於函數式編程,簡單來說,它規定已經實現的功能代碼不允許被修改,但可以被擴展,即:
封閉:已實現的功能代碼塊
開放:對擴展開發
根據這個原則如果要在函數模塊添加功能就不能在內部再添加代碼了,這裏裝飾器就派上用場了。
我們在下面的代碼中會將f1函數添加一個驗證的功能:
def yanzheng(func):
"""裝飾器函數的特點: 基於閉包實現的 有且只有一個參數用於保存被裝飾的函數的引用"""
#這個參數接收的一定是被增加功能的函數的引用
def inner():
print("驗證1......")
func()
return inner
# 在不修改代碼的情況下 對代碼進行功能的擴展
# 裝飾過程: 1 把需要被裝飾的函數的引用傳入到 裝飾器函數內部保存 func中
# 2 創建內部函數 在其中調用func
# 3 在調用func之前可以擴展 新功能
# 4 將裝飾器函數內部的 內部函數的引用傳給 f1<此時的f1就是裝飾功能後的f1>
# 5 對於用戶來講調用f1就可以調用 f1原有函數的功能
# 6 實際上調用f1是執行的inner函數 inner內部中保存的有f1原有的引用
# @裝飾器函數名 語法只是一個語法糖 對靈魂代碼的一種語法封裝
@yanzheng #相當於f1 = yanzheng(f1)在這裏兩個f1是不一樣的,右邊f1是下面f1這個函數的引用,
而左邊f1在傳入yanzheng函數傳入f1參數之後成爲了內部函數inner的引用。在inner函數中我們調用了原本的f1函數,並且附加了打印驗證的功能。
def f1():
print("in f1")
f1()
無參數的函數
無參數的函數
from time import ctime, sleep
def timefun(func):
def wrapped_func():
print("%s called at %s" % (func.__name__, ctime()))
func()
return wrapped_func
@timefun
def foo():
print("I am foo")
foo()
sleep(2)
foo()
上面代碼理解裝飾器執行行爲可理解成
foo = timefun(foo)
# foo先作爲參數賦值給func後,foo接收指向timefun返回的wrapped_func
foo()
# 調用foo(),即等價調用wrapped_func()
# 內部函數wrapped_func被引用,所以外部函數的func變量(自由變量)並沒有釋放
# func裏保存的是原foo函數對象
有參數的函數
被裝飾的函數有參數
from time import ctime, sleep
def timefun(func):
def wrapped_func(a, b):
print("%s called at %s" % (func.__name__, ctime()))
print(a, b)
func(a, b)
return wrapped_func
@timefun
def foo(a, b):
print(a+b)
foo(3,5)
sleep(2)
foo(2,4)
被裝飾的函數有不定長參數
from time import ctime, sleep
def timefun(func):
def wrapped_func(*args, **kwargs):
print("%s called at %s"%(func.__name__, ctime()))
func(*args, **kwargs)
return wrapped_func
@timefun
def foo(a, b, c):
print(a+b+c)
foo(3,5,7)
sleep(2)
foo(2,4,9)
裝飾器中的return
from time import ctime, sleep
def timefun(func):
def wrapped_func():
#先用info 接收一下info 的返回值等待之後的添加的功能執行完畢後再返回
info = func()
print("%s called at %s" % (func.__name__, ctime()))
return info
return wrapped_func
@timefun
def foo():
print("I am foo")
@timefun
def getInfo():
return '----hahah---'
foo()
sleep(2)
foo()
print(getInfo())
裝飾器帶參數,在原有的基礎上,添加外部變量。
from time import ctime, sleep
def timefun_arg(pre="hello"):
def timefun(func):
def wrapped_func():
print("%s called at %s %s" % (func.__name__, ctime(), pre))
return func()
return wrapped_func
return timefun
# 下面的裝飾過程
# 1. 調用timefun_arg("python")
# 2. 將步驟1得到的返回值,即time_fun返回, 然後time_fun(foo)
# 3. 將time_fun(foo)的結果返回,即wrapped_func
# 4. 讓foo = wrapped_fun,即foo現在指向wrapped_func
# 過程整體可以理解爲:把timefun_arg('python')當做一個外部函數的整體,在執行這個函數之後,這個整體就相當於timefun(func),就又變回了我們的基本裝飾器的定義了。而傳入的python參數也可以作爲一個自由變量傳入內部函數當中使用。
@timefun_arg("python") #foo = timefun_arg('python')(foo)
def foo():
print("I am foo")
foo()
sleep(2)
foo()
可以理解爲
foo()==timefun_arg("itcast")(foo)()
類裝飾器
class Test(object):
def __init__(self, func):
print("---初始化---")
print("func name is %s"%func.__name__)
self.__func = func
def __call__(self):
print("---裝飾器中的功能---")
self.__func()
#說明:
#1. 當用Test來裝作裝飾器對test函數進行裝飾的時候,首先會創建Test的實例對象
# 並且會把test這個函數名當做參數傳遞到__init__方法中
# 即在__init__方法中的屬性__func指向了test指向的函數
#
#2. test指向了用Test創建出來的實例對象
#
#3. 當在使用test()進行調用時,就相當於讓這個對象(),因此會調用這個對象的__call__方法
#
#4. 爲了能夠在__call__方法中調用原來test指向的函數體,所以在__init__方法中就需要一個實例屬性來保存這個函數體的引用
# 所以纔有了self.__func = func這句代碼,從而在調用__call__方法中能夠調用到test之前的函數體
@Test
def test():
print("----test---")
test()
showpy()#如果把這句話註釋,重新運行程序,依然會看到"--初始化--"
多個裝飾器裝飾一個函數(較難理解)
# 定義函數:完成包裹數據
def makeBold(fn):
def wrapped():
return "<b>" + fn() + "</b>"
return wrapped
# 定義函數:完成包裹數據
def makeItalic(fn):
def wrapped():
return "<i>" + fn() + "</i>"
return wrapped
@makeBold
def test1():
return "hello world-1"
@makeItalic
def test2():
return "hello world-2"
@makeBold
@makeItalic
def test3():
return "hello world-3"
#這裏我們拆分理解下
test3 = makeBold(test3)
test3 = makeItalic(test3)
程序的執行過程如下,程序會就近原則先執行test3 = makeItalic(test3)這個裝飾器,那麼執行出來的結果test3會是makeItalic中的weapped()的引用,再返回執行上一行的test3 = makeBold(test3),這裏加入的參數test3已經變成了makeItalic中的wrapped()的引用,最後test3指向的是warpped()的引用。
所以在執行test3()的時候的執行流程爲,先執行<b>,然後再執行fn()而此時的fn指向的是makeItalic中的weapped()的引用,接着就會執行<i>然後執行makeItalic的fn,輸出結果hello world-3,fn執行完了再執行</i>注意此時只是makeItalic中warapped()的fn執行完了,哦後面還有</i>將其輸出,此時全部返回。就得到了下面的結果:<b><i>hello world-3</i></b>。
總結出來就是:在裝飾器中實際的代碼是從下到上,實現的過程是從上到下。
print(test1())
print(test2())
print(test3())
運行結果:
<b>hello world-1</b>
<i>hello world-2</i>
<b><i>hello world-3</i></b>
總結:
- 裝飾器函數只有一個參數就是被裝飾的函數的應用
- 裝飾器能夠將一個函數的功能在不修改代碼的情況下進行擴展
- 在函數定義的上方@裝飾器函數名 即可直接使用裝飾器對下面的函數進行裝飾。