引言
通過前面兩篇文章(前兩篇文章見基礎(chǔ)篇, 進(jìn)階篇),讀者們已經(jīng)了解了到了python中的裝飾器背后的實現(xiàn)邏輯舰褪,如何理解python中以@
標(biāo)記的帶裝飾器函數(shù)皆疹,以及如何構(gòu)造帶參數(shù)函數(shù)的裝飾器和動態(tài)生成裝飾器。在接下來這篇文章中占拍,我們將應(yīng)用之前的知識來研究一個比較復(fù)雜的問題——如何構(gòu)造裝飾器的裝飾器,以及裝飾器的最佳實踐捎迫。
構(gòu)造裝飾器的裝飾器
在進(jìn)階篇中我們已經(jīng)知道了如何構(gòu)造一個能接受任意參數(shù)的裝飾器函數(shù)晃酒,同時我們還能夠通過構(gòu)造裝飾器工廠的方式來動態(tài)根據(jù)輸入?yún)?shù)生成裝飾器。接下來我們將應(yīng)用這些知識來實現(xiàn)裝飾器的裝飾器窄绒。這一裝飾器能夠用來裝飾其他裝飾器函數(shù)贝次,從而使得被裝飾的裝飾器函數(shù)能夠接收任意輸入?yún)?shù)(請認(rèn)真讀一讀前面這句邏輯不太直觀的句子,確認(rèn)你已經(jīng)理解了下面代碼的目的)彰导。
這一代碼的有用之處在于蛔翅,我們能夠動態(tài)地將我們的任意一個裝飾器變?yōu)橐粋€能夠接收參數(shù)的裝飾器工廠敲茄。如進(jìn)階篇中所述,由于裝飾器的函數(shù)簽名是固定的——def decorator_func(func_to_decorate)
山析,我們無法在使用@
調(diào)用裝飾器函數(shù)的時候動態(tài)傳入?yún)?shù)堰燎,所以只能先定義一個裝飾器工廠,來替我們接收參數(shù)并返回包含了參數(shù)的閉環(huán)(也就是裝飾器)笋轨。而下面的裝飾器將這一功能抽象了出來秆剪,使得其可以復(fù)用。
代碼如下所示爵政。
def decorator_for_decorator(decorator_to_enhance):
"""
這一函數(shù)用來作為一個裝飾器工廠來動態(tài)生成裝飾器仅讽。
生成的裝飾器能夠被用來裝飾其他裝飾器函數(shù),使得被裝飾的裝飾器函數(shù)變?yōu)槟軌蛉我饨邮諈?shù)的裝飾器钾挟。
"""
# 為了實現(xiàn)參數(shù)的傳遞洁灵,我們在這里動態(tài)生成了一個裝飾器工廠,并作為返回值
# 這一裝飾器工廠將返回一個閉環(huán)作為裝飾器掺出,其中包裝了外界傳入的裝飾器參數(shù)
def decorator_maker(*args, **kwargs):
def decorator_wrapper(func):
# 這里使用了閉環(huán)來保證參數(shù)的傳遞
return decorator_to_enhance(func, *args, **kwargs)
return decorator_wrapper
return decorator_maker
有了這一裝飾器之后处渣,我們就可以動態(tài)改變其他裝飾器了。
# 注意為了保證我們所包裝的裝飾器能夠正確接收參數(shù)蛛砰,我們需要保證其函數(shù)簽名包含我們想要傳入的參數(shù)
# 但這樣的裝飾器函數(shù)是無法直接用來裝飾其他函數(shù)的
@decorator_for_decorator
def decorator_func(func, *args, **kwargs):
def wrapper(function_arg1, function_arg2):
print('Received arguments as {}, {}'.format(args, kwargs))
print('${} per {}, ${} per {}, what would you like to have?'.format(args[0], function_arg1, args[1], function_arg2))
return func(function_arg1, function_arg2)
return wrapper
# 此時罐栈,我們就可以給我們的裝飾器傳入?yún)?shù)啦
@decorator_func(3, 5)
def func(function_arg1, function_arg2):
print('Hello, I would like to have {} and {}'.format(function_arg1, function_arg2))
func('ice cream', 'pizza')
# output:
# Received arguments as (3, 5), {}
# $3 per ice cream, $5 per pizza, what would you like to have?
# Hello, I would like to have ice cream and pizza
上面的代碼可能邏輯上不是那么直觀。我們接下來詳細(xì)分析泥畅。
首先我們定義了一個叫做decorator_maker
的裝飾器函數(shù)荠诬。這一函數(shù)實質(zhì)上是一個能夠返回工廠函數(shù)的函數(shù)。它返回一個能夠返回裝飾器的工廠函數(shù)位仁。也就是說柑贞,被這一裝飾器裝飾過之后,原有的裝飾器函數(shù)將變?yōu)橐粋€裝飾器工廠函數(shù)聂抢。而這一工廠函數(shù)钧嘶,正如我們在進(jìn)階篇中所述,用來接收我們想要傳入的參數(shù)并通過返回一個動態(tài)生成的閉環(huán)作為裝飾器的方式來實現(xiàn)將參數(shù)傳入裝飾器中的目的琳疏。緊接著我們就可以帶參數(shù)通過調(diào)用這個工廠函數(shù)的方式來實現(xiàn)裝飾一個函數(shù)的過程有决。
通過展開裝飾器的方式來理解裝飾的過程
進(jìn)一步地,我們總是可以通過展開裝飾器的方式來理解裝飾的過程發(fā)生了什么空盼。這一方法可以用來分析所有的裝飾器裝飾過的函數(shù)书幕。以上面的代碼為例。
# 我們使用@decorator_for_decorator的方法裝飾了decorator_func函數(shù)揽趾√ɑ悖可以展開如下
def decorator_func(func, *args, **kwargs):
def wrapper(function_arg1, function_arg2):
print('Received arguments as {}, {}'.format(args, kwargs))
print('${} per {}, ${} per {}, what would you like to have?'.format(args[0], function_arg1, args[1], function_arg2))
return func(function_arg1, function_arg2)
return wrapper
decorator_func = decorator_for_decorator(decorator_func)
# 經(jīng)過上面的步驟,decorator_func引用所指向的其實已經(jīng)是decorator_for_decorator所返回的decorator_maker函數(shù)了。這一函數(shù)是一個裝飾器工廠函數(shù)苟呐。
# 緊接著我們帶參數(shù)調(diào)用這一工廠函數(shù)(@decorator_func(3,5))并使用其返回的裝飾器函數(shù)來裝飾另一個函數(shù)(func)痒芝。這一過程展開如下。
true_decorator = decorator_func(3,5)
# 上面的true_decorator引用指向的是decorator_maker所返回的閉環(huán)decorator_wrapper牵素。
def func(function_arg1, function_arg2):
print('Hello, I would like to have {} and {}'.format(function_arg1, function_arg2))
func = true_decorator(func)
# 到上面這一步為止严衬,我們已經(jīng)完成了裝飾func的任務(wù),并且將參數(shù)通過閉環(huán)的方式傳入了我們所使用的裝飾器中两波。
裝飾器的最佳實踐
- 裝飾器實在python2.4中被引入的瞳步。所以要使用裝飾器,需要確保我們使用的python版本>=2.4腰奋。
- 使用裝飾器會減慢調(diào)用函數(shù)的速度单起。
- 一旦一個函數(shù)被裝飾過之后,我們就無法在運(yùn)行時再調(diào)用未裝飾過的原函數(shù)了劣坊。
- 裝飾器事實上只是一個接受函數(shù)作為輸入的函數(shù)嘀倒,并返回一個對原函數(shù)的包裝函數(shù)。這一包裝過程可能會使得debug過程更加復(fù)雜和困難局冰。但在2.5(含)之后的python版本中我們可以使用
functools.wraps()
來降低裝飾器對debug的影響测蘑。
python從2.5開始引入了functools
模塊(module),其中包含的裝飾器函數(shù)functools.wraps()
能夠保證被裝飾函數(shù)的函數(shù)名康二,模塊名碳胳,以及文檔字符串(docstring)被傳入裝飾器返回的包裝函數(shù)中,從而保證了拋出的錯誤信息中能夠包含正確的函數(shù)名沫勿,改善了debug體驗挨约。如下面的代碼所示。
# python的stacktrace信息中包含函數(shù)的__name__屬性來幫助debug
def foo():
pass
print(foo.__name__)
# output: foo
# 但是用了裝飾器之后产雹,__name__屬性會發(fā)生變化
def bar(func):
def wrapper():
return func()
return wrapper
@bar
def foo():
pass
print(foo.__name__)
# output: wrapper
# 通過使用functools來改變這一狀況
import functools
def bar(func):
@functools.wraps(func)
def wrapper():
return func()
return wrapper
@bar
def foo():
pass
print(foo.__name__)
# output: foo
那么诫惭,裝飾器到底有什么用呢?
裝飾器的用法多種多樣蔓挖。舉例來說夕土,我們?nèi)绻胍獢U(kuò)展一個第三方庫中附帶的函數(shù)的功能,但我們又無法修改該函數(shù)源代碼的時候瘟判,我們就可以使用裝飾器來實現(xiàn)這一目的怨绣。或者我們在debug的時候荒适,為了避免對源代碼進(jìn)行多次修改梨熙,就可以用裝飾器來附加我們想要的邏輯。換句話說刀诬,我們可以用裝飾器實現(xiàn)所謂的“干修改”(Dry Change)。
import time
import functools
def benchmark(func):
"""
這是一個能夠計算并打印一個函數(shù)運(yùn)行時間的裝飾器
"""
def wrapper(*args, **kwargs):
start_time = time.time()
res = func(*args, **kwargs)
end_time = time.time()
print('{} completed in {} seconds'.format(func.__name__, end_time - start_time))
return res
return wrapper
def logging(func):
"""
這是一個能夠附加log功能的裝飾器
"""
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
print('{} executed with args: {} and kwargs: {}'.format(func.__name__, args, kwargs))
return res
return wrapper
def counter(func):
"""
這是一個能夠?qū)瘮?shù)被調(diào)用次數(shù)進(jìn)行計數(shù)的裝飾器
"""
def wrapper(*args, **kwargs):
wrapper.count = wrapper.count + 1
res = func(*args, **kwargs)
print('{} has been called for {} times'.format(func.__name__, wrapper.count))
return res
wrapper.count = 0
return wrapper
@counter
@logging
@benchmark
def reverse_string(string):
return ''.join(reversed(string))
reverse_string('Tough times do not last, tough people do.')
# output:
# reverse_string completed in 3.814697265625e-06 seconds
# reverse_string executed with args: ('Tough times do not last, tough people do.',) and kwargs: {}
# reverse_string has been called for 1 times
# '.od elpoep hguot ,tsal ton od semit hguoT'
reverse_string('Two things are infinite: the universe and human stupidity; and I am not sure about the universe.')
# reverse_string completed in 5.9604644775390625e-06 seconds
# reverse_string executed with args: ('Two things are infinite: the universe and human stupidity; and I am not sure about the universe.',) and kwargs: {}
# reverse_string has been called for 2 times
# '.esrevinu eht tuoba erus ton ma I dna ;ytidiputs namuh dna esrevinu eht :etinifni era sgniht owT'
實際上,python自身也提供了一些常用的裝飾器供大家調(diào)用陕壹,例如property
质欲,staticmethod
,等等糠馆。與此同時嘶伟,一些常用的python后端框架,例如Django
及Pyramid
也使用裝飾器來管理緩存以及視圖(view)訪問權(quán)限等又碌。另外九昧,裝飾器有時候也用來在測試中來虛構(gòu)異步請求。
總之毕匀,裝飾器在實際開發(fā)中可以有許多靈活的應(yīng)用铸鹰。讀者朋友們今后可以多加嘗試。
Reference
文中部分內(nèi)容翻譯自如下文章皂岔。翻譯部分版權(quán)歸原作者所有蹋笼。
https://gist.github.com/Zearin/2f40b7b9cfc51132851a