本篇文章帶給大家的內容是關於Python裝飾器的詳細用法介紹(程式碼範例),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。
在Python中,裝飾器一般用來修飾函數,實現公共功能,達到程式碼重複使用的目的。在函數定義前加上@xxxx,然後函數就注入了某些行為,很神奇!然而,這只是語法糖而已。
場景
假設,有一些工作函數,用來對資料做不同的處理:
def work_bar(data): pass def work_foo(data): pass
我們想要在函數呼叫前/後輸出日誌,怎麼辦?
logging.info('begin call work_bar') work_bar(1) logging.info('call work_bar done')
如果有多處程式碼呼叫呢?想想就怕!
傻瓜解法無非是有太多程式碼冗餘,每次函數呼叫都要寫一遍logging
。可以把這部分冗餘邏輯封裝到一個新函數裡:
def smart_work_bar(data): logging.info('begin call: work_bar') work_bar(data) logging.info('call doen: work_bar')
這樣,每次調用smart_work_bar
#即可:
smart_work_bar(1) # ... smart_work_bar(some_data)
看起來挺完美…然而,當work_foo
也有同樣的需要時,還要再實現一次smart_work_foo
嗎?這樣顯然不科學呀!
別急,我們可以用閉包:
def log_call(func): def proxy(*args, **kwargs): logging.info('begin call: {name}'.format(name=func.func_name)) result = func(*args, **kwargs) logging.info('call done: {name}'.format(name=func.func_name)) return result return proxy
這個函數接收一個函數物件(被代理函數)作為參數,傳回一個代理函數。呼叫代理函數時,先輸出日誌,再呼叫被代理函數,呼叫完成後再輸出日誌,最後返回呼叫結果。這樣,不就達到通用化的目的了嗎? ——對於任意被代理函數func
,log_call
皆可輕鬆應付。
smart_work_bar = log_call(work_bar) smart_work_foo = log_call(work_foo) smart_work_bar(1) smart_work_foo(1) # ... smart_work_bar(some_data) smart_work_foo(some_data)
第1
行中,log_call
接收參數work_bar
,傳回一個代理函數proxy
,並賦給smart_work_bar
。在第4
行中,呼叫smart_work_bar
,也就是代理函數proxy
,先輸出日誌,然後呼叫func
也就是 work_bar
,最後再輸出日誌。注意到,代理函數中,func
與傳進去的work_bar
物件緊緊關聯在一起了,這就是閉包。
再提一下,可以覆寫被代理函數名,以smart_
為前綴取新名字還是顯得有些累贅:
work_bar = log_call(work_bar) work_foo = log_call(work_foo) work_bar(1) work_foo(1)
#先來看看以下程式碼:
def work_bar(data): pass work_bar = log_call(work_bar) def work_foo(data): pass work_foo = log_call(work_foo)
雖然程式碼沒有什麼冗餘了,但是看是去還是不夠直覺。這時候,語法糖來了~~~
@log_call def work_bar(data): pass
因此,注意一點(劃重點啦),這裡@log_call
的作用只是:告訴 Python
編譯器插入程式碼work_bar = log_call(work_bar)
。
先來猜猜裝飾器eval_now
有什麼作用?
def eval_now(func): return func()
看起來好奇怪哦,沒有定義代理函數,算裝飾器嗎?
@eval_now def foo(): return 1 print foo
這段程式碼輸出1
,也就是對函數進行呼叫求值。那麼到底有什麼用呢?直接寫foo = 1
不行麼?在這個簡單的例子,這麼寫當然可以啦。來看一個更複雜的範例-初始化一個日誌物件:
# some other code before... # log format formatter = logging.Formatter( '[%(asctime)s] %(process)5d %(levelname) 8s - %(message)s', '%Y-%m-%d %H:%M:%S', ) # stdout handler stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setFormatter(formatter) stdout_handler.setLevel(logging.DEBUG) # stderr handler stderr_handler = logging.StreamHandler(sys.stderr) stderr_handler.setFormatter(formatter) stderr_handler.setLevel(logging.ERROR) # logger object logger = logging.Logger(__name__) logger.setLevel(logging.DEBUG) logger.addHandler(stdout_handler) logger.addHandler(stderr_handler) # again some other code after...
用eval_now
的方式:
# some other code before... @eval_now def logger(): # log format formatter = logging.Formatter( '[%(asctime)s] %(process)5d %(levelname) 8s - %(message)s', '%Y-%m-%d %H:%M:%S', ) # stdout handler stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setFormatter(formatter) stdout_handler.setLevel(logging.DEBUG) # stderr handler stderr_handler = logging.StreamHandler(sys.stderr) stderr_handler.setFormatter(formatter) stderr_handler.setLevel(logging.ERROR) # logger object logger = logging.Logger(__name__) logger.setLevel(logging.DEBUG) logger.addHandler(stdout_handler) logger.addHandler(stderr_handler) return logger # again some other code after...
兩段程式碼要達到的目的是一樣的,但後者顯然更清晰,頗有程式碼塊的風範。更重要的是,函數呼叫在局部名字空間完成初始化,避免臨時變數(如formatter
等)污染外部的名字空間(如全域)。
定義一個裝飾器,用來記錄慢函數呼叫:
def log_slow_call(func): def proxy(*args, **kwargs): start_ts = time.time() result = func(*args, **kwargs) end_ts = time.time() seconds = start_ts - end_ts if seconds > 1: logging.warn('slow call: {name} in {seconds}s'.format( name=func.func_name, seconds=seconds, )) return result return proxy
第3
、5
行分別在函數呼叫前後取樣目前時間,第7
行計算呼叫耗時,耗時大於一秒輸出一條警告日誌。
@log_slow_call def sleep_seconds(seconds): time.sleep(seconds) sleep_seconds(0.1) # 没有日志输出 sleep_seconds(2) # 输出警告日志
然而,閾值設定總是要視情況決定,不同的函數可能會設定不同的值。如果閾值有辦法參數化就好了:
def log_slow_call(func, threshold=1): def proxy(*args, **kwargs): start_ts = time.time() result = func(*args, **kwargs) end_ts = time.time() seconds = start_ts - end_ts if seconds > threshold: logging.warn('slow call: {name} in {seconds}s'.format( name=func.func_name, seconds=seconds, )) return result return proxy
然而,@xxxx
語法糖總是以被裝飾函數為參數呼叫裝飾器,也就是說沒有機會傳遞 threshold
參數。怎麼辦呢? ——用一個閉包封裝threshold
參數:
def log_slow_call(threshold=1): def decorator(func): def proxy(*args, **kwargs): start_ts = time.time() result = func(*args, **kwargs) end_ts = time.time() seconds = start_ts - end_ts if seconds > threshold: logging.warn('slow call: {name} in {seconds}s'.format( name=func.func_name, seconds=seconds, )) return result return proxy return decorator @log_slow_call(threshold=0.5) def sleep_seconds(seconds): time.sleep(seconds)
這樣,log_slow_call(threshold=0.5)
呼叫回傳函數decorator
,函數擁有閉包變數threshold
,值為0.5
。 decorator
再裝飾sleep_seconds
。
採用預設閾值,函數呼叫還是不能省略:
@log_slow_call() def sleep_seconds(seconds): time.sleep(seconds)
處女座可能會對第一行這對括號感到不爽,那麼可以這樣改進:
def log_slow_call(func=None, threshold=1): def decorator(func): def proxy(*args, **kwargs): start_ts = time.time() result = func(*args, **kwargs) end_ts = time.time() seconds = start_ts - end_ts if seconds > threshold: logging.warn('slow call: {name} in {seconds}s'.format( name=func.func_name, seconds=seconds, )) return result return proxy if func is None: return decorator else: return decorator(func)
這種寫法相容於兩種不同的用法,用法A
預設閾值(無呼叫);用法B
自訂閾值(有呼叫)。
# Case A @log_slow_call def sleep_seconds(seconds): time.sleep(seconds) # Case B @log_slow_call(threshold=0.5) def sleep_seconds(seconds): time.sleep(seconds)
用法A
中,發生的事情是log_slow_call(sleep_seconds)
,也就是func
參數是非空的,這是直接調decorator
進行包裝並返回(閾值是預設的)。
用法B
中,先发生的是log_slow_call(threshold=0.5)
,func
参数为空,直接返回新的装饰器decorator
,关联闭包变量threshold
,值为0.5
;然后,decorator
再装饰函数sleep_seconds
,即decorator(sleep_seconds)
。注意到,此时threshold
关联的值是0.5
,完成定制化。
你可能注意到了,这里最好使用关键字参数这种调用方式——使用位置参数会很丑陋:
# Case B- @log_slow_call(None, 0.5) def sleep_seconds(seconds): time.sleep(seconds)
当然了,函数调用尽量使用关键字参数是一种极佳实践,含义清晰,在参数很多的情况下更是如此。
上节介绍的写法,嵌套层次较多,如果每个类似的装饰器都用这种方法实现,还是比较费劲的(脑子不够用),也比较容易出错。
假设有一个智能装饰器smart_decorator
,修饰装饰器log_slow_call
,便可获得同样的能力。这样,log_slow_call
定义将变得更清晰,实现起来也更省力啦:
@smart_decorator def log_slow_call(func, threshold=1): def proxy(*args, **kwargs): start_ts = time.time() result = func(*args, **kwargs) end_ts = time.time() seconds = start_ts - end_ts if seconds > threshold: logging.warn('slow call: {name} in {seconds}s'.format( name=func.func_name, seconds=seconds, )) return result return proxy
脑洞开完,smart_decorator
如何实现呢?其实也简单:
def smart_decorator(decorator): def decorator_proxy(func=None, **kwargs): if func is not None: return decorator(func=func, **kwargs) def decorator_proxy(func): return decorator(func=func, **kwargs) return decorator_proxy return decorator_proxy
smart_decorator
实现了以后,设想就成立了!这时,log_slow_call
,就是decorator_proxy
(外层),关联的闭包变量decorator
是本节最开始定义的log_slow_call
(为了避免歧义,称为real_log_slow_call
)。log_slow_call
支持以下各种用法:
# Case A @log_slow_call def sleep_seconds(seconds): time.sleep(seconds)
用法A
中,执行的是decorator_proxy(sleep_seconds)
(外层),func
非空,kwargs
为空;直接执行decorator(func=func, **kwargs)
,即real_log_slow_call(sleep_seconds)
,结果是关联默认参数的proxy
。
# Case B # Same to Case A @log_slow_call() def sleep_seconds(seconds): time.sleep(seconds)
用法B
中,先执行decorator_proxy()
,func
及kwargs
均为空,返回decorator_proxy
对象(内层);再执行decorator_proxy(sleep_seconds)
(内层);最后执行decorator(func, **kwargs)
,等价于real_log_slow_call(sleep_seconds)
,效果与用法A
一致。
# Case C @log_slow_call(threshold=0.5) def sleep_seconds(seconds): time.sleep(seconds)
用法C
中,先执行decorator_proxy(threshold=0.5)
,func
为空但kwargs
非空,返回decorator_proxy
对象(内层);再执行decorator_proxy(sleep_seconds)
(内层);最后执行decorator(sleep_seconds, **kwargs)
,等价于real_log_slow_call(sleep_seconds, threshold=0.5)
,阈值实现自定义!
以上是Python裝飾器的詳細用法介紹(程式碼範例)的詳細內容。更多資訊請關注PHP中文網其他相關文章!