Python3 CookBook | 元編程(一)

1、在函數上添加包裝器

【問題】
你想在函數上添加一個包裝器,增加額外的操作處理(比如日誌、計時等)

【解決方法】
如果你想使用額外的代碼包裝一個函數,可以定義一個裝飾器,如下:

def timethis(func):
    @wraps(func)
    def wrapper(*args,**kwargs):
        start = time.time()
        result = func(*args,**kwargs)
        end = time.time()
        print(func.__name__,end-start)
        return result
    return wrapper

@timethis
def countdown(n):
    while n > 0:
        n -= 1

countdown(1000000)

# countdown 0.044847965240478516

一個裝飾器就是一個函數,它接受一個函數作爲參數並返回一個新的函數。當你像下面這樣寫:

@timethis
def countdown(n):
	pass

就跟下面這樣寫其實效果是一樣的:

def countdown(n):
	pass
countdown = timethis(countdown)

順便說一下,內置的裝飾器比如 @staticmethod,@classmethod,@property原理也是一樣的。例如,下面這兩個代碼片段是等價的:

class A:
	@classmethod
	def method(cls):
		pass
class B:
	def method(cls):
		pass
	method = classmethod(method)

在上面的wapper()函數中,裝飾器內部定義了一個使用*args**kwargs來接受任意參數的函數。在這個函數裏面調用了原始函數並將其結果返回,不過你還可以添加其他額外代碼。然後這個新的函數包裝器作爲返回結果返回來替代原始函數。

2、創建裝飾器時保留函數元信息

【問題】
你寫了一個裝飾器作用在某函數上,但是這個函數的重要的元信息比如名字、文檔字符串、註解和參數簽名都丟失了。

【解決方案】
任何時候你定義裝飾器的時候,都應該使用 functools 庫中的 @wraps 裝飾器來註解底層包裝函數。例如:

def timethis(func):
    @wraps(func)
    def wrapper(*args,**kwargs):
        start = time.time()
        result = func(*args,**kwargs)
        end = time.time()
        print(func.__name__,end-start)
        return result
    return wrapper

下面我們使用這個被包裝後的函數並檢查它的元信息

@timethis
def countdown(n):
    while n > 0:
        n -= 1

countdown(1000000)
print(countdown.__name__)
print(countdown.__doc__)

#countdown 0.04883718490600586
#countdown
#None

在編寫裝飾器的時候複製元信息是一個非常重要的部分,如果你忘記了使用@wraps,那麼你會發現被裝飾函數丟失了所有有用的信息。

3、解除一個裝飾器

【問題】
一個裝飾器已經作用在一個函數上,你想撤銷它,直接訪問原始的未包裝的那個函數。

【解決方案】
假設裝飾器是通過@wraps來實現的,那麼你可以通過訪問__wrapped__屬相來訪問原始函數

@somedecorator
def add(x,y):
	return x,y

orig_add = add.__wrapped__
orig_add(3,4)
# 7

直接訪問未包裝的原始函數在調試、內省和其他函數操作時很有用的。但是我們這裏的方案僅僅用於在包裝器正確使用了 @wraps 或者直接設置了__wrapped__屬性的情況。

如果有多個包裝器,那麼訪問__wrapped__屬性的行爲是不可預知的,應該避免這樣做。

3、定義一個帶參數的裝飾器

【問題】
你想定義一個可以接受參數的裝飾器
【解決方案】
我們用一個例子詳細闡述接受參數的處理過程。假設你想寫一個裝飾器,給函數添加日誌功能,當時允許用戶指定日誌的級別和其他的選項。下面這個裝飾器的定義和使用示例:

from functools import wraps
import logging

def logged(level,name=None,message=None):
    def decorate(func):
        logname = name if name else func.__module__
        log = logging.getLogger(logname)
        logmsg = message if message else func.__name__

        @wraps(func)
        def wrapper(*args,**kwargs):
            log.log(level,logmsg)
            return func(*args,**kwargs)
        return wrapper
    return decorate

@logged(logging.DEBUG)
def add(x,y):
    return x + y

@logged(logging.CRITICAL,'example')
def spam():
    print('Spam!')

初看起來,這種實現看上去很複雜,但是核心思想很簡單。最外層的函數 logged()接受參數並將他們作用在內部的裝飾器函數上面。內層的函數 decorate() 接受一個函數作爲參數,然後再函數上面放置一個裝飾器。這裏的關鍵點是包裝器是可以傳遞給logged()的參數的。

定義一個接受參數的包裝器看上去比較複雜主要原因是底層的調用序列。特別的,如果你有如下這個代碼:

@decorator(x,y,z)
def func(a,b):
	pass

裝飾器處理過程跟下面的調用是等效的:

def func(a,b):
	pass
func = decorator(x,y,z)(func)

decorator(x,y,z)的返回結果必須是一個可調用的對象,他接受一個函數作爲參數幷包裝它。

未完待續 …

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章