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

有時候我們想為多個函數(shù),同意添加某一種功能,比如及時統(tǒng)計,記錄日志,緩存運算結(jié)果等等,而又不想改變函數(shù)代碼
那就定義裝飾器函數(shù),用它來生成一個在原函數(shù)基礎(chǔ)添加了新功能的函數(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)過程中有很多重復(fù)的步驟,所以這樣很浪費

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

關(guān)于尾遞歸參照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)計,記錄日志,緩存運算結(jié)果等等
而又不想改變函數(shù)代碼
定義裝飾器函數(shù),用它來生成一個在原函數(shù)基礎(chǔ)添加了新功能的函數(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__

運行結(jié)果:

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 其實就對應(yīng)著(‘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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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