1. 程式人生 > >python wraps那點兒事兒

python wraps那點兒事兒

wrpas

一個需求的實現

當前,我們有這麽一個小的需求:通過裝飾器來計算函數執行的時間

計算出這個函數的執行時長

def add(x,y):   # add = TimeIt(add)
    time.sleep(1)
    'this is add'
    return x + y



裝飾器實現

import time
import datetime
from functools import wraps
class TimeIt:
    def __init__(self,fn):
        print('init')
        self._fn = fn
    def __call__(self, *args, **kwargs):
        start = datetime.datetime.now()
        ret = self._fn(*args, **kwargs)
        delta = datetime.datetime.now() - start
        print(delta)
        return ret
@TimeIt
def add(x,y):   # add = TimeIt(add)
    time.sleep(1)
    'this is add'
    return x + y
add(1,2)
print(add.__doc__)
print(add.__name__)


我們所看到的信息如下:

Traceback (most recent call last):
  File "H:/Python_Project/test2/3.py", line 33, in <module>
    print(add.__name__)
AttributeError: 'TimeIt' object has no attribute '__name__'


那麽問題來了,在打印__doc__ 和 __name__ 的時候看到返回的並非是我們想要的,因為已經被包裝到TimeIt中的可調用對象,所以,現在它是一個實例了,實例是不能調用__name__的;所以,我們來手動模擬一下,將其偽裝寫入__doc__ 和 __name__



改造

手動拷貝:粗糙的改造方式,將其__doc__ __name__強行復制到實例中

self無非是我們當前所綁定的類實例,fn是通過裝飾器傳遞進來的add,我們將fn的doc 和 name 作為源強行的賦值到self中,如下:

class TimeIt:
    def __init__(self,fn):
        print('init')
        self._fn = fn
# 函數的doc 拷貝到 fn中
        self.__doc__ = self._fn.__doc__ 
        self.__name__ = self._fn.__name__

這樣效果肯定是不好的,這樣做就是為了得知其保存位置,那麽接下來引入wraps模塊


引入wraps

wraps本質是一個函數裝飾器,通過接收一個參數再接收一個參數進行傳遞並處理,反正網上也一堆使用方法,舉例不再說明,但是這裏需要將函數調用的等價式摸清

使用方式:

from functools import wraps
def looger(fn):
    @wraps(fn)       
    def wrapper(*args, **kwargs):
        xxxxxxxx

等價式關系 : @wraps(fn) = ( a = wraps(fn); a(wrapper) )

可以看出,源是傳遞進來的fn,目標是self,也就是wrapper


過程分析

首先我們通過編輯器跟進到函數內部


def wraps(wrapped,
       assigned = WRAPPER_ASSIGNMENTS,
       updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function
       Returns a decorator that invokes update_wrapper() with the decorated
       function as the wrapper argument and the arguments to wraps() as the
       remaining arguments. Default arguments are as for update_wrapper().
       This is a convenience function to simplify applying partial() to
       update_wrapper().
    """

可看到wraps中,需要傳遞幾個參數,跟進到assigned,被包裝的函數才是src源,也就是說被外部的更新掉

查看 assigned = WRAPPER_ASSIGNMENTS

通過WRAPPER_ASSIGNMENTS 發現是被跳轉到了

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):

可看到wraps中,需要傳遞幾個參數,跟進到assigned,被包裝的函數才是src源,也就是說被外部的更新掉
查看 assigned = WRAPPER_ASSIGNMENTS

那麽賦值更新哪些東西呢?就是這些屬性,如下所示
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')

而updated = WRAPPER_UPDATES 所覆蓋的則就是從WRAPPER_UPDATES = ('__dict__',)的基礎上在執行了更新操作WRAPPER_ASSIGNMENTS,說白了全是在當前__dict__中進行
如果存在字典之類的屬性要做的是並不是覆蓋字典,而是在他們的字典中將自身的信息覆蓋或增加等更新操作
assigned 只有默認值,但是夠我們用了

對象屬性的訪問
繼續往下查看代碼:
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
# Issue #17482: set __wrapped__ last so we don't inadvertently copy it
# from the wrapped function when updating __dict__
wrapper.__wrapped__ = wrapped
# Return the wrapper so this can be used as a decorator via partial()
return wrapper

它是通過反射機制通過找到__dict__,如果存在則返回,沒有則觸發setattr將value寫入到__dict__ 
value = getattr(wrapped, attr) 從attr反射獲取了屬性,attr就是assigent,而assigent就是WRAPPER_ASSIGNMENTS 定義的屬性
setattr(wrapper, attr, value) 如果沒有找到則動態的加入到其字典中
wrapper.__wrapped__ = wrapped 將wrapper拿到之後為其加入了一個屬性,也屬於一個功能增強,把wrapperd 也就是被包裝函數,將add的引用交給了def wrapper(*args, **kwargs) ; 凡是被包裝過的都會增加這個屬性

說白了 wraps就是調用了update_wrapper,只不過少了一層傳遞

那麽再回到wraps中(這下面為啥刷不出來格式?)
def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):


是不是感覺少了些東西?實際它是調用了partial偏函數
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)

通過偏函數,update_wrapper 對應的wrapper ,送入一個函數,其他 照單全收
接下來又會引入一個新的函數,partial具體分析後期再寫
總之一句話:wraps 是通過裝飾器方式進行傳參並增強,將需要一些基礎屬性以反射的方式從源中賦值到當前dict中,並使用偏函數生成了一個新的函數並返回


python wraps那點兒事兒