python 裝飾器以及開發(fā)中常用的例子

有時候我們想為多個函數(shù)在张,同意添加某一種功能用含,比如及時統(tǒng)計,記錄日志帮匾,緩存運算結果等等啄骇,而又不想改變函數(shù)代碼
那就定義裝飾器函數(shù),用它來生成一個在原函數(shù)基礎添加了新功能的函數(shù)瘟斜,代替原函數(shù)
參考金角大王的博客

裝飾器從無到有的過程

比如現(xiàn)在有三個已經(jīng)實現(xiàn)功能的函數(shù)

def shoping():
    print 'shoping'
    
def info():
    print 'information'
    
def talking():
    print 'talking'

然后然后想給每個模塊加一個登陸驗證功能

user_status = False # 先定義變量

def login():
    _username = "ketchup" #假裝這是DB里存的用戶信息
    _password = "123456" #假裝這是DB里存的用戶信息
    global user_status
 
    if user_status == False:
        username = input("user:")
        password = input("pasword:")
 
        if username == _username and password == _password:
            print("welcome login....")
            user_status = True
        else:
            print("wrong username or password!")
    else:
        print("用戶已登錄缸夹,驗證通過...")

一開始的想法是這樣痪寻,在每個函數(shù)中添加函數(shù)

def shoping():
    login()
    print 'shoping'
    
def info():
    login()
    print 'info'
    
def talking():
    login()
    print 'talking'

但是,軟件開發(fā)要遵循‘開放封閉的原則’虽惭,它規(guī)定已經(jīng)實現(xiàn)的功能代碼不允許被修改橡类,但可以被擴展,
封閉:就是已經(jīng)實現(xiàn)功能的代碼塊芽唇,盡量不在內(nèi)部做修改
開放:對擴展開發(fā)

然后修改為:

user_status = False # 先定義變量

def login(func):
    _username = "ketchup" #假裝這是DB里存的用戶信息
    _password = "123456" #假裝這是DB里存的用戶信息
    global user_status
 
    if user_status == False:
        username = input("user:")
        password = input("pasword:")
 
        if username == _username and password == _password:
            print("welcome login....")
            user_status = True
        else:
            print("wrong username or password!")
    if user_status == True:
        func() #如果登陸成功顾画,就調(diào)用傳入的函數(shù)
    
def shoping():
    print 'shoping'
    
def info():
    print 'info'
    
def talking():
    print 'talking' 
    
    
login(shoping) #這樣,先執(zhí)行l(wèi)ogin函數(shù)匆笤,在login函數(shù)內(nèi)部就會調(diào)用執(zhí)行shoping函數(shù) 
login(info)
login(talking)

但是每次這樣調(diào)用研侣,如果很多模塊要加這個功能,大家都要去調(diào)用一下炮捧,那太麻煩了</br>
python中一切皆對象庶诡,可以用

shopping = login(shoping)
shopping()

這樣的方式執(zhí)行shopping()就等于執(zhí)行l(wèi)ogin(shopping)</br>
但是在前面賦值 shopping = login(shoping)的時候,就已經(jīng)調(diào)用login()函數(shù)了寓盗,執(zhí)行了里面的func()函數(shù)
要解決這個問題灌砖,就要在shopping = login(shoping)這次調(diào)用的時候,不執(zhí)行func函數(shù)傀蚌,只是把一個函數(shù)名給了他基显,然后下面shoppin()函數(shù)執(zhí)行的時候才會執(zhí)行,
所以善炫,就要在login函數(shù)里加一層閉包函數(shù)

def login(func):
    def wrapper():
        _username = "ketchup" #假裝這是DB里存的用戶信息
        _password = "123456" #假裝這是DB里存的用戶信息
        global user_status
     
        if user_status == False:
            username = input("user:")
            password = input("pasword:")
     
            if username == _username and password == _password:
                print("welcome login....")
                user_status = True
            else:
                print("wrong username or password!")
        if user_status == True:
            func() #如果登陸成功撩幽,就調(diào)用傳入的函數(shù)
    return wrapper

這樣的話,第一次shopping = login(shopping) 的時候箩艺,shopping 的值為wrapper
后面

def shoping():
    print 'shoping'

的時候窜醉,才執(zhí)行wrapper() ,才調(diào)用里面的func()

然后python對這種寫法有一個語法糖 這種寫法就等于在shopping函數(shù)前面加上@login

如果要在shopping里面?zhèn)鲄?shù)怎么辦呢艺谆?
那就要在login里面把參數(shù)拿過來榨惰,然后傳給func

def login(func):
    def wrapper(*args,**kwargs):
        _username = "ketchup" #假裝這是DB里存的用戶信息
        _password = "123456" #假裝這是DB里存的用戶信息
        global user_status
     
        if user_status == False:
            username = input("user:")
            password = input("pasword:")
     
            if username == _username and password == _password:
                print("welcome login....")
                user_status = True
            else:
                print("wrong username or password!")
        if user_status == True:
            func(*args,**kwargs) #如果登陸成功,就調(diào)用傳入的函數(shù)
    return wrapper
    
@login
def shoping(num):       
    print 'shoping %d 個'%num
    
@login  
def info(): 
    print 'info'
    
@login  
def talking():      
    print 'talking' 

如果這時候要對login傳參數(shù)呢静汤,那就在多加一層對login參數(shù)的判斷琅催,比如,要判斷是從qq還是weixin登陸的

def login(auth_type):
    def auth(func):
        def wrapper(*args,**kwargs):
            if auth_type == 'qq':
                _username = "ketchup" #假裝這是DB里存的用戶信息
                _password = "123456" #假裝這是DB里存的用戶信息
                global user_status

                if user_status == False:
                    username = input("user:")
                    password = input("pasword:")

                    if username == _username and password == _password:
                        print("welcome login....")
                        user_status = True
                    else:
                        print("wrong username or password!")
                if user_status == True:
                    func(*args,**kwargs) #如果登陸成功虫给,就調(diào)用傳入的函數(shù)
            else:
                print('only support qq or weixin ')
        return wrapper
    return auth

下面以幾個實際問題的例子深入理解裝飾器

1-實現(xiàn)斐波那契的幾個方法

為什么要用裝飾器實現(xiàn)斐波那契藤抡,因為實現(xiàn)過程中有很多重復的步驟,所以這樣很浪費

image.png
def memo(func):
    cache = {}
    def wrap(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrap
@memo
def fib1(n):
    if n<=1:
        return 1
    return fib1(n-1) + fib1(n-2)
    
    

def fib2(n,cache = None):
    if cache is None:
        cache = {}
    if n in cache:
        return cache[n]
    if n<= 1:
        return 1
    cache[n] = fib2(n-1,cache) + fib2(n-2,cache)
    return cache[n]
    
    
def fib3(n):
    a,b = 1,1
    while n >= 2:
        a,b = b, a+b
        n -= 1
    return b
    
    
    
def fib4(n):
    li = [1,1]
    while n >=2:
        li.append(li[-2]+li[-1])
        n -= 1
    return li[-1]

測試:

    / print(fib1(500))
    print(fib2(500))
    print(fib3(500))
    print(fib4(500))

22559151616193633087251269503607207204601132491375819058863886641847462773868688340
5015987052796968498626
22559151616193633087251269503607207204601132491375819058863886641847462773868688340
5015987052796968498626
22559151616193633087251269503607207204601132491375819058863886641847462773868688340
5015987052796968498626

當?shù)?00 的時候抹估,fib1已經(jīng)報錯了缠黍,RecursionError: maximum recursion depth exceeded in comparison

報錯是因為每一級遞歸都需要調(diào)用函數(shù), 會創(chuàng)建新的棧,
隨著遞歸深度的增加, 創(chuàng)建的棧越來越多, 造成爆棧

當1000的時候,fib2 也報這個錯誤了
因為python 不支持尾遞歸药蜻,所以超過1000也會報錯


ERROR

關于尾遞歸參照Python開啟尾遞歸優(yōu)化

下面小練習:
如果有n級臺階瓷式,每一次可以跨1-3級臺階替饿,那么可以有多少種走法

@memo
def climb(n, steps):
    count = 0
    if n == 0:
        count =1
    elif n>0:
        for step in steps:
            count += climb(n - step, steps)
    return count
    #測試
print climb(200,(1,2,3))

52622583840983769603765180599790256716084480555530641

有時候我們想為多個函數(shù),同意添加某一種功能蒿往,比如及時統(tǒng)計盛垦,記錄日志,緩存運算結果等等
而又不想改變函數(shù)代碼
定義裝飾器函數(shù)瓤漏,用它來生成一個在原函數(shù)基礎添加了新功能的函數(shù)腾夯,代替原函數(shù)

2-為被裝飾的函數(shù)保留原來的元數(shù)據(jù)

解決方法:
使用標準庫functools中的裝飾器wraps裝飾內(nèi)部包裹函數(shù),可以定制原函>數(shù)的某些屬性蔬充,更新到包裹函數(shù)上面

先看一下函數(shù)的元數(shù)據(jù)一般有哪些:

f.name : 函數(shù)的名字</br>
f.doc : 函數(shù)的文檔字符串蝶俱,對這個函數(shù)的一些描述</br>
f.moudle : 函數(shù)所屬的模塊名</br>
f.dict : 屬性字典</br>
f.defaults : 默認參數(shù)元祖</br>

In [1]: def f():
   ...:     '''f doc'''
   ...:     print('ffff')
   ...: 
In [11]: f.__doc__
Out[11]: 'f doc'

In [12]: f.__module__
Out[12]: '__main__'

In [13]: f.__defaults__

In [14]: def f(a, b=1, c=[]):
    ...:     print a,b,c
    ...:     

In [15]: f.__defaults__
Out[15]: (1, [])

In [17]: f.__defaults__[1].append('abc')
In [19]: f(5)
    5 1 ['abc']

所以在默認參數(shù)里盡量不要使用可變對象

In [20]: f.__closure__

In [21]: def f():
    ...:     a = 2
    ...:     return lambda k:a**k
    ...: 

In [22]: g = f()
In [27]: g.__closure__
Out[27]: (<cell at 0x11013af68: int object at 0x7ff0b05056e0>,)

In [28]: c = g.__closure__[0]

In [29]: c
Out[29]: <cell at 0x11013af68: int object at 0x7ff0b05056e0>

In [30]: c.cell_contents
Out[30]: 2

問題:

我們在使用裝飾器裝飾函數(shù)了之后,查看函數(shù)的元數(shù)據(jù)會顯示是裝飾器函數(shù)的元數(shù)據(jù)饥漫,而不是原函數(shù)的
def mydecorator(func):
    def wrapper(*args,**kwargs):
        '''wrapper function'''
        print 'in wrapper'
    return wrapper

@mydecorator
def example():
    '''example function'''
    print 'in example'

print example.__name__
print example.__doc__

運行結果:

wrapper</br>
wrapper function

解決1:
def mydecorator(func):
    def wrapper(*args,**kwargs):
        '''wrapper function'''
        wrapper.__name__ = func.__name__
        print 'in wrapper'
    return wrapper

但是代碼很不優(yōu)雅

解決2:
from functools import update_wrapper

def mydecorator(func):
    def wrapper(*args,**kwargs):
        '''wrapper function'''
        update_wrapper(wrapper, func, ('__name__', '__doc__'), ('__dic__'))
        print 'in wrapper'
    return wrapper

functools 里有兩個默認參數(shù)榨呆,WRAPPER_ASSIGNMENTS,WRAPPER_UPDATES 其實就對應著(‘module’, 'name', 'doc'), ('dic'),)
所以可以直接不用寫,這兩個是默認帶的

解決3:
from functools import update_wrapper

def mydecorator(func):
    def wrapper(*args,**kwargs):
        '''wrapper function'''
        update_wrapper(wrapper, func)
        print 'in wrapper'
    return wrapper

最后來說這個wraps庸队,這個wraps 就是一個邊界函數(shù)积蜻,他也是一個裝飾器,是一個帶參數(shù)的裝飾器彻消,可以直接用

使用標準庫functools中的裝飾器wraps裝飾內(nèi)部包裹函數(shù)竿拆,可以定制原函數(shù)的某些屬性,更新到包裹函數(shù)上面

解決end:
from functools import update_wrapper,wraps

def mydecorator(func):
    @wraps(func)
    def wrapper(*args,**kwargs):
        '''wrapper function'''
        #update_wrapper(wrapper, func)
        print 'in wrapper'
    return wrapper

3-定義帶參數(shù)的裝飾器

實現(xiàn)一個裝飾器宾尚,他用來檢查唄裝飾函數(shù)的參數(shù)類型丙笋,裝飾器可以通過參數(shù)致命函數(shù)參數(shù)的類型,調(diào)用時如果檢測出類型不匹配則拋出異常

@typeassert(str,int,int)
def f(a,b,c):
    ....

@typeassert(y= list)
def g(x,y):
    ...

解決方案煌贴, 帶參數(shù)的裝飾器也就是根據(jù)參數(shù)定制出一個裝飾器可以看成生產(chǎn)裝飾器的工廠御板,每次調(diào)用typeassert 返回一個特定的裝飾器,然后用它去裝飾其他函數(shù)

from inspect import signature

def typeassert(*ty_args, **ty_kargs):
    def decorator(func):
        # func -> a,b 
        # d = {'a': int, 'b': str}
        sig = signature(func)
        btypes = sig.bind_partial(*ty_args, **ty_kargs).arguments
        def wrapper(*args, **kargs):
            # arg in d, instance(arg, d[arg])
            for name, obj in sig.bind(*args, **kargs).arguments.items():
                if name in btypes:
                    if not isinstance(obj, btypes[name]):
                        raise TypeError('"%s" must be "%s"' % (name, btypes[name]))
            return func(*args, **kargs)
        return wrapper
    return decorator

@typeassert(int, str, list)
def f(a, b, c):
    print(a, b, c)

測試

f(1,'666',[1,2,3])
f(1,2,[])

TypeError: 'b' must be '<class 'str'

我們來看看signature 是怎么用的

In [1]: from inspect import signature
In [2]: def f(a, b, c=1):pass
In [16]: c = sig.parameters

In [17]: c
Out[17]: 
mappingproxy({'a': <Parameter "a">,
              'b': <Parameter "b">,
              'c': <Parameter "c=1">})

In [18]: c = sig.parameters['c']
In [20]: c.default
Out[20]: 1  

如果想對a b c 簡歷一個類型的字典{‘a(chǎn)’:'int','b':'str','c':'list'}

In [23]: bargs = sig.bind(str,int,int)

In [24]: bargs.args
Out[24]: (str, int, int)

In [26]: bargs.arguments
Out[26]: OrderedDict([('a', str), ('b', int), ('c', int)])

In [29]: bargs.arguments['a']
Out[29]: str

但是如果sig.bind(str)
只傳了一個參數(shù)牛郑,就會報錯怠肋,如果想只穿一個參數(shù)的話,就用
sig.bind_partial(str)

例:寫一個出錯重試次數(shù)的裝飾器淹朋,可以用來處理HTTP超時等

import requests
def retry(attempt):
    def decorator(fuc):
        def wrapper(*args, **kw):
            att = 0
            while att < 3:
                try:
                    return func(*args, **kw)
                except Exception as e:
                    att += 1
        return wrapper
    return decorator
    
#重試次數(shù)
@retry(attempt=3)
def get_response(url):
    r = resquests.get('www.baidu.com')
    return r.content
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末灶似,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子瑞你,更是在濱河造成了極大的恐慌,老刑警劉巖希痴,帶你破解...
    沈念sama閱讀 211,348評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件者甲,死亡現(xiàn)場離奇詭異,居然都是意外死亡砌创,警方通過查閱死者的電腦和手機虏缸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評論 2 385
  • 文/潘曉璐 我一進店門鲫懒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人刽辙,你說我怎么就攤上這事窥岩。” “怎么了宰缤?”我有些...
    開封第一講書人閱讀 156,936評論 0 347
  • 文/不壞的土叔 我叫張陵颂翼,是天一觀的道長。 經(jīng)常有香客問我慨灭,道長朦乏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,427評論 1 283
  • 正文 為了忘掉前任氧骤,我火速辦了婚禮呻疹,結果婚禮上,老公的妹妹穿的比我還像新娘筹陵。我一直安慰自己刽锤,他們只是感情好,可當我...
    茶點故事閱讀 65,467評論 6 385
  • 文/花漫 我一把揭開白布朦佩。 她就那樣靜靜地躺著并思,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吕粗。 梳的紋絲不亂的頭發(fā)上纺荧,一...
    開封第一講書人閱讀 49,785評論 1 290
  • 那天,我揣著相機與錄音颅筋,去河邊找鬼宙暇。 笑死,一個胖子當著我的面吹牛议泵,可吹牛的內(nèi)容都是我干的占贫。 我是一名探鬼主播,決...
    沈念sama閱讀 38,931評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼先口,長吁一口氣:“原來是場噩夢啊……” “哼型奥!你這毒婦竟也來了?” 一聲冷哼從身側響起碉京,我...
    開封第一講書人閱讀 37,696評論 0 266
  • 序言:老撾萬榮一對情侶失蹤厢汹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后谐宙,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烫葬,經(jīng)...
    沈念sama閱讀 44,141評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,483評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了搭综。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片垢箕。...
    茶點故事閱讀 38,625評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖兑巾,靈堂內(nèi)的尸體忽然破棺而出条获,到底是詐尸還是另有隱情,我是刑警寧澤蒋歌,帶...
    沈念sama閱讀 34,291評論 4 329
  • 正文 年R本政府宣布帅掘,位于F島的核電站,受9級特大地震影響奋姿,放射性物質(zhì)發(fā)生泄漏锄开。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,892評論 3 312
  • 文/蒙蒙 一称诗、第九天 我趴在偏房一處隱蔽的房頂上張望萍悴。 院中可真熱鬧,春花似錦寓免、人聲如沸癣诱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撕予。三九已至,卻和暖如春蜈首,著一層夾襖步出監(jiān)牢的瞬間实抡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工欢策, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吆寨,地道東北人。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓踩寇,卻偏偏與公主長得像啄清,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子俺孙,可洞房花燭夜當晚...
    茶點故事閱讀 43,492評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 裝飾器 裝飾器本質(zhì)上是一個Python函數(shù)辣卒,它可以讓其他函數(shù)在不需要做任何代碼變動的前提下增加額外功能,裝飾器的返...
    時間之友閱讀 2,248評論 0 3
  • 每個人都有的內(nèi)褲主要功能是用來遮羞睛榄,但是到了冬天它沒法為我們防風御寒荣茫,咋辦?我們想到的一個辦法就是把內(nèi)褲改造一下场靴,...
    chen_000閱讀 1,360評論 0 3
  • 雖然人們能利用函數(shù)閉包(function clouser)寫出簡單的裝飾器啡莉,但其可用范圍常受限制。多數(shù)實現(xiàn)裝飾器的...
    gomibako閱讀 1,015評論 0 4
  • 天空下著微微細雨,嫣然站在學校門口票罐,望著天空。 雨意連綿不斷泞边,越來越大该押,身邊的同學要么打著傘緩步前行,要么把書包阵谚、...
    月亮背面gy閱讀 189評論 0 1
  • “我想蚕礼,在牡丹眼中,能使生活美滿的梢什,只有愛情奠蹬。”這是我在讀完《紅牡丹》這本書之后覺得用來形容牡丹的最為貼切的一...
    李戈瓦閱讀 392評論 0 0