裝飾器
此文可能是有史以來最全的關於Python裝飾器的Blog了...
函數名的運用
關於函數名
函數名是⼀個變量,但它是⼀個特殊的變量。與括號配合可以執⾏函數的變量。
查看函數名的內存地址:
def func(): print('呵呵') print(func) # <function func at 0x10983c048>
做變量
def func(): print('呵呵') a = func # 把函數當成變量賦值給另外一個變量 a() # 通過變量a調用函數
做容器的元素
def func1(): print('func1') def func2(): print('func2') def func3(): print('func3') def func4(): print('func4') list1 = [func1, func2, func3, func4] for i in list1: i()
做參數
def func1(): print('func1') def func2(arg): print('start') arg() # 執行傳遞進來的arg print('end') func2(func1) # 把func1當成參數傳遞給func2
做返回值
def func1(): print('這里是func1') def func2(): print('這里是func2') return func2 # 把func2當成返回值返回 ret = func1() # 調用func1,把返回值賦值給ret ret() # 調用ret
閉包
靈魂三問
首先我們來看一個例子:
def func1(): name = '張三' def func2(arg): print(arg) func2(name) func1()
理解了上面的例子,我們再看一個例子:
def func1(): name = '張三' def func2(): print(name) # 能夠訪問到外層作用域的變量 func2() func1()
最后再看一個例子:
def func1(name): def func2(): print(name) # 能夠訪問到外層作用域的變量 func2() func1('張三')
閉包的定義
一個內層函數中,引用了外層函數(非全局)的變量,這個內層函數就可以成為閉包。
在Python中,我們可以使用__closure__來檢測函數是否是閉包。
def func1(): name = '張三' def func2(): print(name) # 能夠訪問到外層作用域的變量 func2() print(func2.__closure__) # (<cell at 0x1036c7438: str object at 0x10389d088>,) func1() print(func1.__closure__) # None
問題來了,我們如何在函數外邊調用函數內部的函數呢?
當然是把內部函數當成返回值返回了。
def func1(): name = '張三' def func2(): print(name) return func2 # 把內部函數當成是返回值返回 ret = func1() # 把返回值賦值給變量ret ret() # 調用內部函數
內部函數當然還可包含其他的函數,多層嵌套的道理都是一樣的。
def func1(): def func2(): def func3(): print('func3') return func3 return func2 ret1 = func1() # func2 ret2 = ret1() # func3 ret2()
接下來我們看下面這個例子,來更深刻的理解一下閉包的含義:
def print_msg(msg): # 這是外層函數 def printer(): # 這是內層函數 print(msg) return printer # 返回內層函數 func = print_msg("Hello") func()
現在我們進行如下操作:
>>> del print_msg >>> func() Hello >>> print_msg("Hello") Traceback (most recent call last): ... NameError: name 'print_msg' is not defined
我們知道如果⼀個函數執⾏完畢,則這個函數中的變量以及局部命名空間中的內容都將會被銷毀。在閉包中內部函數會引用外層函數的變量,而且這個變量將不會隨着外層函數的結束而銷毀,它會在內存中保留。
也就是說,閉包函數可以保留其用到的變量的引用。
閉包面試題
# 編寫代碼實現func函數,使其實現以下效果: foo = func(8) print(foo(8)) # 輸出64 print(foo(-1)) # 輸出-8
裝飾器
裝飾器來歷
在說裝飾器之前,我們先說⼀個軟件設計的原則: 開閉原則, ⼜被成為開放封閉原則。
開放封閉原則是指對擴展代碼的功能是開放的,但是對修改源代碼是封閉的。這樣的軟件設計思路可以保證我們更好的開發和維護我們的代碼。
我們先來寫一個例子,模擬一下女媧造人:
def create_people(): print('女媧真厲害,捏個泥吹口氣就成了人!') create_people()
好吧,現在問題來了。上古時期啊,天氣很不穩定,這個時候突然大旱三年。女媧再去捏人啊,因為太干了就捏不到一塊兒去了,需要在捏人之前灑點水才行。
def create_people(): print('灑點水') print('女媧真厲害,捏個泥吹口氣就成了人!') create_people()
這不就搞定了么?但是呢,我們是不是違背了開放封閉原則呢?我們是添加了新的功能,但是我們是直接修改了源代碼。在軟件開發中我們應該對直接修改源代碼是謹慎的。
比如,女媧為了防止浪費,想用剩下點泥巴捏個雞、鴨、鵝什么的,也需要灑點水。那我們能在每個造雞、造鴨、造鵝函數的源代碼中都手動添加代碼么?肯定是不現實的。
怎么辦?再寫一個函數不就OK了么?
def create_people(): print('女媧真厲害,捏個泥吹口氣就成了人!') def create_people_with_water(): print('灑點水') create_people() create_people_with_water()
不讓我直接修改源代碼,那我重新寫一個函數不就可以了嗎?
但是,你有沒有想過一個問題,女媧造人也很累的,她后來開了很多分店,每家分店都是調用了之前的create_people函數造人,那么你修改了之后,是不是所有調用原來函數的人都需要修改調用函數的名稱呢?很麻煩啊!!!
總結一句話就是如何在不改變函數的結構和調用方式的基礎上,動態的給函數添加功能?
def create_people(): print('女媧真厲害,捏個泥吹口氣就成了人!') def a(func): def b(): print('灑點水') func() return b ret = a(create_people) ret()
利用閉包函數不就可以了么?
但是,你這最后調用的是ret啊,不還是改變了調用方式么?
再往下看:
def create_people(): print('女媧真厲害,捏個泥吹口氣就成了人!') def a(func): def b(): print('灑點水') func() return b create_people = a(create_people) create_people()
上面這段代碼是不是完美解決了我們的問題呢?
看一下它的執行過程吧:
- 首先訪問a(create_people)
- 把create_people函數賦值給了a函數的形參func,記住后續執行func的話實際上是執行了最開始傳入的create_people函數。
- a函數執行過程就是一句話,返回了b函數。這個時候把b函數賦值給了create_people這個變量
- 執行create_people的時候,相當於執行了b函數,先打印灑點水再執行func,也就是我們最開始傳入的create_people函數
我們巧妙的使用閉包實現了,把一個函數包裝了一下,然后再賦值給原來的函數名。
裝飾器語法糖
上面的代碼就是一個裝飾器的雛形,Python中針對於上面的功能提供了一個快捷的寫法,俗稱裝飾器語法糖。
使用裝飾器語法糖的寫法,實現同樣功能的代碼如下:
def a(func): def b(): print('灑點水') func() return b @a # 裝飾器語法糖 def create_people(): print('女媧真厲害,捏個泥吹口氣就成了人!') create_people()
裝飾器進階
裝飾帶返回值的函數
如果被裝飾的函數有返回值,我們應該怎么處理呢?
請看下面的示例:
def foo(func): # 接收的參數是一個函數名 def bar(): # 定義一個內層函數 print("這里是新功能...") # 新功能 r = func() # 在內存函數中拿到被裝飾函數的結果 return r # 返回被裝飾函數的執行結果 return bar # 定義一個有返回值的函數 @foo def f1(): return '嘿嘿嘿' # 調用被裝飾函數 ret = f1() # 調用被裝飾函數並拿到結果 print(ret)
裝飾帶參數的函數
def foo(func): # 接收的參數是一個函數名 def bar(x, y): # 這里需要定義和被裝飾函數相同的參數 print("這里是新功能...") # 新功能 func(x, y) # 被裝飾函數名和參數都有了,就能執行被裝飾函數了 return bar # 定義一個需要兩個參數的函數 @foo def f1(x, y): print("{}+{}={}".format(x, y, x+y)) # 調用被裝飾函數 f1(100, 200)
帶參數的裝飾器
被裝飾的函數可以帶參數,裝飾器同樣也可以帶參數。
回頭看我們上面寫得那些裝飾器,它們默認把被裝飾的函數當成唯一的參數。但是呢,有時候我們需要為我們的裝飾器傳遞參數,這種情況下應該怎么辦呢?
接下來,我們就一步步實現帶參數的裝飾器:
首先我們來回顧下上面的代碼:
def f1(func): # f1是我們定義的裝飾器函數,func是被裝飾的函數 def f2(*arg, **kwargs): # *args和**kwargs是被裝飾函數的參數 func(*arg, **kwargs) return f2
從上面的代碼,我們發現了什么?
我的裝飾器如果有參數的話,沒地方寫了…怎么辦呢?
還是要使用閉包函數!
我們需要知道,函數除了可以嵌套兩層,還能嵌套更多層:
# 三層嵌套的函數 def f1(): def f2(): name = "張三" def f3(): print(name) return f3 return f2
嵌套三層之后的函數調用:
f = f1() # f --> f2 ff = f() # ff --> f3 ff() # ff() --> f3() --> print(name) --> 張三
注意:在內部函數f3中能夠訪問到它外層函數f2中定義的變量,當然也可以訪問到它最外層函數f1中定義的變量。
# 三層嵌套的函數2 def f1(): name = '張三' def f2(): def f3(): print(name) return f3 return f2
調用:
f = f1() # f --> f2 ff = f() # ff --> f3 ff() # ff() --> f3() --> print(name) --> 張三
好了,現在我們就可以實現我們的帶參數的裝飾器函數了:
# 帶參數的裝飾器需要定義一個三層的嵌套函數 def d(name): # d是新添加的最外層函數,為我們原來的裝飾器傳遞參數,name就是我們要傳遞的函數 def f1(func): # f1是我們原來的裝飾器函數,func是被裝飾的函數 def f2(*arg, **kwargs): # f2是內部函數,*args和**kwargs是被裝飾函數的參數 print(name) # 使用裝飾器函數的參數 func(*arg, **kwargs) # 調用被裝飾的函數 return f2 return f1
上面就是一個帶參裝飾器的代碼示例,現在我們來寫一個完整的應用:
def d(a=None): # 定義一個外層函數,給裝飾器傳參數--role def foo(func): # foo是我們原來的裝飾器函數,func是被裝飾的函數 def bar(*args, **kwargs): # args和kwargs是被裝飾器函數的參數 # 根據裝飾器的參數做一些邏輯判斷 if a: print("歡迎來到{}頁面。".format(a)) else: print("歡迎來到首頁。") # 調用被裝飾的函數,接收參數args和kwargs func(*args, **kwargs) return bar return foo @d() # 不給裝飾器傳參數,使用默認的'None'參數 def index(name): print("Hello {}.".format(name)) @d("電影") # 給裝飾器傳一個'電影'參數 def movie(name): print("Hello {}.".format(name)) if __name__ == '__main__': index('張三') movie('張三')
裝飾器修復技術
被裝飾的函數最終都會失去本來的__doc__等信息, Python給我們提供了一個修復被裝飾函數的工具。
def a(func): @wraps(func) def b(): print('灑點水') func() return b @a # 裝飾器語法糖 def create_people(): """這是一個女媧造人的功能函數""" print('女媧真厲害,捏個泥吹口氣就成了人!') create_people() print(create_people.__doc__) print(create_people.__name__)
多個裝飾器裝飾同一函數
同一個函數可以被多個裝飾器裝飾,此時需要注意裝飾器的執行順序。
def foo1(func): print("d1") def inner1(): print("inner1") return "<i>{}</i>".format(func()) return inner1 def foo2(func): print("d2") def inner2(): print("inner2") return "<b>{}</b>".format(func()) return inner2 @foo1 @foo2 def f1(): return "Hello Andy" # f1 = foo2(f1) ==> print("d2") ==> f1 = inner2 # f1 = foo1(f1) ==> print("d1") ==> f1 = foo1(inner2) ==> inner1 ret = f1() # 調用f1() ==> inner1() ==> <i>inner2()</i> ==> <i><b>inner1()</b></i> ==> <i><b>Hello Andy</b></i> print(ret)
裝飾器終極進階
類裝飾器
我們除了可以使用函數裝飾函數外,還可以用類裝飾函數。
class D(object): def __init__(self, a=None): self.a = a self.mode = "裝飾" def __call__(self, *args, **kwargs): if self.mode == "裝飾": self.func = args[0] # 默認第一個參數是被裝飾的函數 self.mode = "調用" return self # 當self.mode == "調用"時,執行下面的代碼(也就是調用使用類裝飾的函數時執行) if self.a: print("歡迎來到{}頁面。".format(self.a)) else: print("歡迎來到首頁。") self.func(*args, **kwargs) @D() def index(name): print("Hello {}.".format(name)) @D("電影") def movie(name): print("Hello {}.".format(name)) if __name__ == '__main__': index('張三') movie('張三')
裝飾類
我們上面所有的例子都是裝飾一個函數,返回一個可執行函數。Python中的裝飾器除了能裝飾函數外,還能裝飾類。
可以使用裝飾器,來批量修改被裝飾類的某些方法:
# 定義一個類裝飾器 class D(object): def __call__(self, cls): class Inner(cls): # 重寫被裝飾類的f方法 def f(self): print('Hello 張三.') return Inner @D() class C(object): # 被裝飾的類 # 有一個實例方法 def f(self): print("Hello world.") if __name__ == '__main__': c = C() c.f()
舉個實際的應用示例:
我們把類中的一個只讀屬性定義為property屬性方法,只有在訪問它時才參與計算,一旦訪問了該屬性,我們就把這個值緩存起來,下次再訪問的時候無需重新計算。
class lazyproperty: def __init__(self, func): self.func = func def __get__(self, instance, owner): if instance is None: return self else: value = self.func(instance) setattr(instance, self.func.__name__, value) return value import math class Circle: def __init__(self, radius): self.radius = radius @lazyproperty def area(self): print('計算面積') return math.pi * self.radius ** 2 c1 = Circle(10) print(c1.area) print(c1.area)