Python的閉包和裝飾器
閉包
變數作用域
作用域是程式執行時變數可以被引用的範圍。
- 函式內部的變數被稱為區域性變數,它只能在函式內部中被引用。
- 定義在程式碼最外層的變數為全域性變數,它可以整個程式碼中被引用。
在函式內部可以訪問全域性變數,在函式外部不能訪問區域性變數。
函式巢狀
把一個函式定義在另外一個函式的內部,就是函式巢狀。外邊的函式為外層函式,裡邊的函式為內層函式。
閉包
在函式巢狀中,內層函式對外層函式的區域性變數進行了引用,並且外層函式的返回值是內層函式的引用,就構成了一個閉包。
def outer(a, b): a = a b = b def inside(): print(a+b) return inside function_inside = outer(10, 20) function_inside()
執行結果:
30
- 函式是一個物件,可以作為返回值被返回,也可以作為引數被傳遞。
- 函式執行完畢後變數會被回收,但是因為內層函式對外部函式的變數進行了引用,所以即使外層函式執行完畢,變數也不會被回收。
修改外層函式的變數使用nonlocal關鍵字
建立一個閉包
- 閉包函式必須有函式巢狀
- 內層函式需要引用外層函式中的變數
閉包函式必須返回內層函式的引用
注意點
閉包就是函式和函式獨有的資料 結合在一起,它比類要輕量級,比一般的函式功能更強。
每次呼叫外層函式都會建立一個新的閉包物件。
內層函式沒有引用的變數會在外層函式執行結束後銷燬。
裝飾器
問題提出
- 當電腦的硬碟空間不夠用的時候。
- 我們能想到的一個辦法就是把電腦拆開,再新增一塊硬碟。(功能增強)
- 但是這樣做,不僅麻煩,而且改變了原有的電腦結構。(複雜,改變了原有函式)
- 我們不想做改變原有電腦這麼複雜的事情,但是又要儲存更多東西。
- 所以聰明的人就發明了行動硬碟,它可以在不改變原有結構的情況下,又做了功能增強,並且還可以為被多個電腦使用,一臺電腦也可以使用多個行動硬碟。(功能增強,方便,不改變原有函式)
而我們需要給函式增加新的功能,又不想改變函式的程式碼的時候,便用到了 裝飾器
python函式
python函式可以被當作引數傳遞給其他函式。
def say(func): func() def test(): print("this is a function") say(test)
執行結果:
this is a function
python的函式可以像變數一樣作為返回值返回,被定義在函式內部,而且還可以作為引數被傳遞。
裝飾器
原有函式:
def test():
print("this is a function")
現在需要給函式增加另外一個功能,限制該函式的呼叫。
print("use limit")
把功能加入到函式內部
我們可以直接把功能加入到函式內部:
def test():
print("use limit")
print("this is a function")
test()
但是如果有函式test1、test2()...也想使用該功能,我們就需要一一修改,這就造成了:
- 修改工作量巨大
- 造成大量的重複程式碼
- 不利於以後功能的新增
定義一個新的函式
我們可以定義一個新的函式,來完成新增加的功能,把原有函式當作引數傳入新函式執行。
def limit(func):
print("use limit")
func()
def test():
print("this is a function")
limit(test)
這樣不僅沒修改原有的函式,而且增加了新的功能,但是也存在新的問題:
- 呼叫的時候,不是在呼叫
test
而是呼叫新函式limit
- 每次使用
test
函式的時候都需要呼叫limit
,如果有已存在的呼叫,將無法使用新加入的功能,會對程式碼結構造成破環。
使用裝飾器
如果想對函式增加新的功能,並且不修改原有函式,且呼叫方式不做出改變的話,就要使用裝飾器。
簡單的裝飾器實現:
def limit(func):
def addlimit():
print("use limit")
func()
return addlimit
def test():
print("this is a function")
test = limit(test)
test()
函式 limit
就是一個裝飾器,它把函式 test
當作引數傳入,在內層函式中增加功能後,又把內層函式返回,重新賦值給 test
變數。這裡使用到了閉包,外層函式負責接收要修飾的函式,返回修飾後的函式,內層函式賦值修飾傳入的函式。
裝飾器的進階
@語法糖
裝飾器的使用有一種簡寫方式,就是在函式定義之前使用 @
+裝飾器名字:
def limit(func):
def addlimit():
print("use limit")
func()
return addlimit
@limit # 相當於test = limit(test)
def test():
print("this is a function")
test()
@limit
便相當於 test = limit(test)
。
使用@語法糖 便相當於把定義在後邊的函式當作引數傳入裝飾器。
- 可以省去最後一步再賦值的操作,使用方便。
- 可以不修改原有的函式增加新的功能。
- 不改變函式的呼叫方式。
- 可以為多個函式進行裝飾。
被裝飾函式帶有引數
def limit(func):
def addlimit(a, b):
print("use limit")
func(a, b)
return addlimit
@limit
def test(a, b):
print("I tell you : %s" % a)
print("I tell you : %s" % b)
a = "hello"
b = "hi"
test(a, b)
因為裝飾器呼叫的是閉包中的內部函式,所以我們先在內部函式接收引數,再傳遞給被裝飾的函式。這樣,引數經過傳遞便被傳遞給了原有的 test
函式。
但是,如果其他函式不是兩個引數,在使用該裝飾器的時候,便會執行錯誤,為了裝飾器的通用性,我們可以用不定長位置引數 *args
和關鍵字引數 **kwargs
使用不定長引數
def limit(func):
def addlimit(*args, **kwargs):
print("use limit")
func(*args, **kwargs)
return addlimit
@limit
def test(a, b):
print("I tell you : %s" % a)
print("I tell you : %s" % b)
@limit
def test1(a, b, c):
print("I tell you : %s" % a)
print("I tell you : %s" % b)
print("I tell you : %s" % c)
a = "hello"
b = "hi"
test(a, b)
print()
test1(a, b, c = "hello world")
執行結果:
use limit
I tell you : hello
I tell you : hi
use limit
I tell you : hello
I tell you : hi
I tell you : hello world
被裝飾函式帶有返回值
def limit(func):
def addlimit():
print("use limit")
return func()
return addlimit
@limit
def test():
print("this is a function")
return "I tell you: hello"
print(test())
直接在閉包內部函式返回原函式的呼叫結果即可。
既有引數又有返回值的通用裝飾器
def limit(func):
def addlimit(*args, **kwargs):
print("use limit")
return func(*args, **kwargs)
return addlimit
三層裝飾器
三層裝飾器可以在原有裝飾器的基礎上,設定額外的外部變數。
def limit_arg(arg):
def limit(func):
def addlimit():
print("use limit--%s" % arg)
func()
return addlimit
return limit
@limit_arg("hello")
def test():
print("this is a function")
test()
執行結果:
use limit--hello
this is a function
test()
相當於:
limit_arg("hello")(test)()
類裝飾器
裝飾器不僅是一個函式,還可以是一個類。
class Limit(object):
def __init__(self, func):
self.func = func
def __call__(self):
print("use limit")
self.func()
@Limit
def test():
print("this is a function")
test()
魔術方法
__call__
方法可以讓類的例項物件像函式一樣被呼叫。類裝飾器對比函式裝飾器具有靈活度大、高內聚、封裝性等優點。
多個裝飾器
def add_a(func):
def a():
return "<a href='mxuanli.cn'>" + func() + "</a>"
return a
def add_h1(func):
def h1():
return "<h1>" + func() + "</h1>"
return h1
@add_a
@add_h1
def test():
return "hello world"
print(test())
執行結果:
<a href='mxuanli.cn'><h1>hello world</h1></a>
- 多個裝飾器的時候,執行順序是從裡到外,會先從最內層的開始執行。