Python 裝飾器【轉】
相關
函數作用域LEGB:L>E>G>B
- L:local函數內部作用域
- E:enclosing函數內部與內嵌函數之間(閉包)
- G:global全局作用域
- B:build-in內置作用域
閉包Closure概念:內部函數中隊enclosing作用域的變量進行引用,將該變量放到Closure的屬性中,當內部函數處理時,可以直接使用該變量。
函數實質與屬性
- 函數是一個對象
- 函數執行完成後內部變量回收
- 函數屬性
- 函數返回值
閉包作用:1.封裝 2.代碼復用
裝飾器
裝飾器其實就是閉包所帶來的一個語法糖
裝飾器
由於函數也是一個對象,而且函數對象可以被賦值給變量,所以,通過變量也能調用該函數。
>>> from time import ctime
>>> def now():
... return ctime()
...
>>> f = now
>>> f()
'Sat Feb 24 16:43:28 2018'
函數對象有一個__name__
屬性,可以拿到函數的名字:
>>> now.__name__
'now'
>>> f.__name__
'now'
現在,假設我們要增強now()
函數的功能,比如,在函數調用前後自動打印日誌,但又不希望修改now()
本質上,decorator就是一個返回函數的高階函數。所以,我們要定義一個能打印日誌的decorator,可以定義如下:
>>> def log(func):
... def wrapper(*args, **kw):
... print('call %s():' % func.__name__)
... return func(*args, **kw)
... return wrapper
觀察上面的log
,因為它是一個decorator,所以接受一個函數作為參數,並返回一個函數。我們要借助Python的@語法,把decorator置於函數的定義處:
@log
def now():
print '2013-12-25'
調用now()
函數,不僅會運行now()
函數本身,還會在運行now()
函數前打印一行日誌:
>>> @log
... def now():
... print(ctime())
...
...
>>> now()
call now():
Sat Feb 24 16:47:53 2018
把@log
放到now()
函數的定義處,相當於執行了語句:
now = log(now)
由於log()
是一個decorator,返回一個函數,所以,原來的now()
函數仍然存在,只是現在同名的now變量指向了新的函數,於是調用now()
將執行新函數,即在log()
函數中返回的wrapper()
函數。
wrapper()
函數的參數定義是(*args, **kw)
,因此,wrapper()
函數可以接受任意參數的調用。在wrapper()
函數內,首先打印日誌,再緊接著調用原始函數。
如果decorator本身需要傳入參數,那就需要編寫一個返回decorator的高階函數,寫出來會更復雜。比如,要自定義log的文本:
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print '%s %s():' % (text, func.__name__)
return func(*args, **kw)
return wrapper
return decorator
這個3層嵌套的decorator用法如下:
@log('execute')
def now():
print '2013-12-25'
執行結果如下:
>>> now()
execute now():
2013-12-25
和兩層嵌套的decorator相比,3層嵌套的效果是這樣的:
>>> now = log('execute')(now)
我們來剖析上面的語句,首先執行log(‘execute‘)
,返回的是decorator
函數,再調用返回的函數,參數是now
函數,返回值最終是wrapper
函數。
以上兩種decorator的定義都沒有問題,但還差最後一步。因為我們講了函數也是對象,它有__name__
等屬性,但你去看經過decorator裝飾之後的函數,它們的__name__
已經從原來的‘now‘
變成了‘wrapper‘
:
>>> now.__name__
'wrapper'
因為返回的那個wrapper()
函數名字就是‘wrapper‘
,所以,需要把原始函數的__name__
等屬性復制到wrapper()
函數中,否則,有些依賴函數簽名的代碼執行就會出錯。
不需要編寫wrapper.__name__ = func.__name__
這樣的代碼,Python內置的functools.wraps
就是幹這個事的,所以,一個完整的decorator的寫法如下:
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print 'call %s():' % func.__name__
return func(*args, **kw)
return wrapper
或者針對帶參數的decorator:
import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print '%s %s():' % (text, func.__name__)
return func(*args, **kw)
return wrapper
return decorator
import functools
是導入functools
模塊。模塊的概念稍候講解。現在,只需記住在定義wrapper()
的前面加上@functools.wraps(func)
即可。
在面向對象(OOP)的設計模式中,decorator被稱為裝飾模式。OOP的裝飾模式需要通過繼承和組合來實現,而Python除了能支持OOP的decorator外,直接從語法層次支持decorator。Python的decorator可以用函數實現,也可以用類實現。
decorator可以增強函數的功能,定義起來雖然有點復雜,但使用起來非常靈活和方便。
請編寫一個decorator,能在函數調用的前後打印出‘begin call‘
和‘end call‘
的日誌。
再思考一下能否寫出一個@log
的decorator,使它既支持:
@log
def f():
pass
又支持:
@log('execute')
def f():
pass
偏函數
Python的functools
模塊提供了很多有用的功能,其中一個就是偏函數(Partial function)。要註意,這裏的偏函數和數學意義上的偏函數不一樣。
在介紹函數參數的時候,我們講到,通過設定參數的默認值,可以降低函數調用的難度。而偏函數也可以做到這一點。舉例如下:
int()
函數可以把字符串轉換為整數,當僅傳入字符串時,int()
函數默認按十進制轉換:
>>> int('12345')
12345
但int()
函數還提供額外的base
參數,默認值為10
。如果傳入base
參數,就可以做N進制的轉換:
>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565
假設要轉換大量的二進制字符串,每次都傳入int(x, base=2)
非常麻煩,於是,我們想到,可以定義一個int2()
的函數,默認把base=2
傳進去:
def int2(x, base=2):
return int(x, base)
這樣,我們轉換二進制就非常方便了:
>>> int2('1000000')
64
>>> int2('1010101')
85
functools.partial
就是幫助我們創建一個偏函數的,不需要我們自己定義int2()
,可以直接使用下面的代碼創建一個新的函數int2
:
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85
所以,簡單總結functools.partial
的作用就是,把一個函數的某些參數給固定住(也就是設置默認值),返回一個新的函數,調用這個新函數會更簡單。
註意到上面的新的int2
函數,僅僅是把base
參數重新設定默認值為2
,但也可以在函數調用時傳入其他值:
>>> int2('1000000', base=10)
1000000
最後,創建偏函數時,實際上可以接收函數對象、*args
和**kw
這3個參數,當傳入:
int2 = functools.partial(int, base=2)
實際上固定了int()函數的關鍵字參數base
,也就是:
int2('10010')
相當於:
kw = { base: 2 }
int('10010', **kw)
當傳入:
max2 = functools.partial(max, 10)
實際上會把10
作為*args
的一部分自動加到左邊,也就是:
max2(5, 6, 7)
相當於:
args = (10, 5, 6, 7)
max(*args)
結果為10
。
參考
來源於慕課網教程筆記
Python 裝飾器【轉】