python 装饰器以及开发中常用的例子

有时候我们想为多个函数,同意添加某一种功能,比如及时统计,记录日志,缓存运算结果等等,而又不想改变函数代码
那就定义装饰器函数,用它来生成一个在原函数基础添加了新功能的函数,代替原函数
参考金角大王的博客

装饰器从无到有的过程

比如现在有三个已经实现功能的函数

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("用户已登录,验证通过...")

一开始的想法是这样,在每个函数中添加函数

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

但是,软件开发要遵循‘开放封闭的原则’,它规定已经实现的功能代码不允许被修改,但可以被扩展,
封闭:就是已经实现功能的代码块,尽量不在内部做修改
开放:对扩展开发

然后修改为:

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() #如果登陆成功,就调用传入的函数
    
def shoping():
    print 'shoping'
    
def info():
    print 'info'
    
def talking():
    print 'talking' 
    
    
login(shoping) #这样,先执行login函数,在login函数内部就会调用执行shoping函数 
login(info)
login(talking)

但是每次这样调用,如果很多模块要加这个功能,大家都要去调用一下,那太麻烦了</br>
python中一切皆对象,可以用

shopping = login(shoping)
shopping()

这样的方式执行shopping()就等于执行login(shopping)</br>
但是在前面赋值 shopping = login(shoping)的时候,就已经调用login()函数了,执行了里面的func()函数
要解决这个问题,就要在shopping = login(shoping)这次调用的时候,不执行func函数,只是把一个函数名给了他,然后下面shoppin()函数执行的时候才会执行,
所以,就要在login函数里加一层闭包函数

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() #如果登陆成功,就调用传入的函数
    return wrapper

这样的话,第一次shopping = login(shopping) 的时候,shopping 的值为wrapper
后面

def shoping():
    print 'shoping'

的时候,才执行wrapper() ,才调用里面的func()

然后python对这种写法有一个语法糖 这种写法就等于在shopping函数前面加上@login

如果要在shopping里面传参数怎么办呢?
那就要在login里面把参数拿过来,然后传给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) #如果登陆成功,就调用传入的函数
    return wrapper
    
@login
def shoping(num):       
    print 'shoping %d 个'%num
    
@login  
def info(): 
    print 'info'
    
@login  
def talking():      
    print 'talking' 

如果这时候要对login传参数呢,那就在多加一层对login参数的判断,比如,要判断是从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) #如果登陆成功,就调用传入的函数
            else:
                print('only support qq or weixin ')
        return wrapper
    return auth

下面以几个实际问题的例子深入理解装饰器

1-实现斐波那契的几个方法

为什么要用装饰器实现斐波那契,因为实现过程中有很多重复的步骤,所以这样很浪费

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

当到500 的时候,fib1已经报错了,RecursionError: maximum recursion depth exceeded in comparison

报错是因为每一级递归都需要调用函数, 会创建新的栈,
随着递归深度的增加, 创建的栈越来越多, 造成爆栈

当1000的时候,fib2 也报这个错误了
因为python 不支持尾递归,所以超过1000也会报错


ERROR

关于尾递归参照Python开启尾递归优化

下面小练习:
如果有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

有时候我们想为多个函数,同意添加某一种功能,比如及时统计,记录日志,缓存运算结果等等
而又不想改变函数代码
定义装饰器函数,用它来生成一个在原函数基础添加了新功能的函数,代替原函数

2-为被装饰的函数保留原来的元数据

解决方法:
使用标准库functools中的装饰器wraps装饰内部包裹函数,可以定制原函>数的某些属性,更新到包裹函数上面

先看一下函数的元数据一般有哪些:

f.name : 函数的名字</br>
f.doc : 函数的文档字符串,对这个函数的一些描述</br>
f.moudle : 函数所属的模块名</br>
f.dict : 属性字典</br>
f.defaults : 默认参数元祖</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']

所以在默认参数里尽量不要使用可变对象

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

问题:

我们在使用装饰器装饰函数了之后,查看函数的元数据会显示是装饰器函数的元数据,而不是原函数的
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

但是代码很不优雅

解决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 里有两个默认参数,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 就是一个边界函数,他也是一个装饰器,是一个带参数的装饰器,可以直接用

使用标准库functools中的装饰器wraps装饰内部包裹函数,可以定制原函数的某些属性,更新到包裹函数上面

解决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-定义带参数的装饰器

实现一个装饰器,他用来检查呗装饰函数的参数类型,装饰器可以通过参数致命函数参数的类型,调用时如果检测出类型不匹配则抛出异常

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

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

解决方案, 带参数的装饰器也就是根据参数定制出一个装饰器可以看成生产装饰器的工厂,每次调用typeassert 返回一个特定的装饰器,然后用它去装饰其他函数

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’:'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)
只传了一个参数,就会报错,如果想只穿一个参数的话,就用
sig.bind_partial(str)

例:写一个出错重试次数的装饰器,可以用来处理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
    
#重试次数
@retry(attempt=3)
def get_response(url):
    r = resquests.get('www.baidu.com')
    return r.content
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,841评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,415评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,904评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,051评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,055评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,255评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,729评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,377评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,517评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,420评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,467评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,144评论 3 317
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,735评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,812评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,029评论 1 256
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,528评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,126评论 2 341

推荐阅读更多精彩内容

  • 装饰器 装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返...
    时间之友阅读 2,240评论 0 3
  • 每个人都有的内裤主要功能是用来遮羞,但是到了冬天它没法为我们防风御寒,咋办?我们想到的一个办法就是把内裤改造一下,...
    chen_000阅读 1,360评论 0 3
  • 虽然人们能利用函数闭包(function clouser)写出简单的装饰器,但其可用范围常受限制。多数实现装饰器的...
    gomibako阅读 1,009评论 0 4
  • 天空下着微微细雨,嫣然站在学校门口,望着天空。 雨意连绵不断,越来越大,身边的同学要么打着伞缓步前行,要么把书包、...
    月亮背面gy阅读 187评论 0 1
  • “我想,在牡丹眼中,能使生活美满的,只有爱情。”这是我在读完《红牡丹》这本书之后觉得用来形容牡丹的最为贴切的一...
    李戈瓦阅读 385评论 0 0