美文网首页python基础
python进阶:第八章(装饰器使用技巧)

python进阶:第八章(装饰器使用技巧)

作者: 码农小杨 | 来源:发表于2017-05-02 15:35 被阅读34次

    问题一:如何使用函数装饰器?

    问题内容:
    某些时候我们想为多个函数,统一添加某种功能,比如计时统计,记录日志,缓存运算结果等等。

    题目一:
    裴波那契数列(Fibonacci Sequence),又称黄金分割数列,指的是这样一个数列:1,1,2,3,5,8,13,21,.......,这个数列从第三项开始,每一项都等于前两项之和,求数列第n项。

    In [1]: def fibonacci(n):
       ...:     if n<= 1:
       ...:         return 1
       ...:     return fibonacci(n - 1) + fibonacci(n - 2)
       ...:
    

    上面的方法没有使用缓存,每次计算都要重复计算前面的数值。

    In [2]: def fibonacci(n,cache):
       ...:     if cache is None:
       ...:         cache = {}
       ...:
       ...:     if n in cache:
       ...:         return cache[n]
       ...:
       ...:     if n<= 1:
       ...:         return 1
       ...:     cache[n] =  fibonacci(n - 1,cache) + fibonacci(n - 2,cache)
       ...:     return cache[n]
       ...:
       ...:
    

    我们引入缓存,这样计算的速度将会加快很多。

    In [8]: print(fibonacci(20,{}))
    10946
    

    题目2:一个共有10个台阶的楼梯,从下面走到上面,一次只能迈1-3个台阶,并且不能后退,走完这个楼梯共有多少种方法。

    In [9]: 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
       ...:
    

    使用装饰器:

    In [16]: def memo(func):
        ...:     cache = {}
        ...:     def warp(*args):
        ...:         if args not in cache:
        ...:             cache[args] = func(*args)
        ...:         return cache[args]
        ...:     return warp
        ...:
    
    In [17]: @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
        ...:
    
    In [18]: climb(10,(1,2,3))
    Out[18]: 274
    

    如何为被装饰的函数保存元数据?

    问题内容:
    在函数对象中保存着一些函数的元数据,例如:

    f.__name__    :    函数的名字
    f.__doc__    :    函数文档字符串
    f.__moudle__    :    函数所属模块名
    f.__dict__    :    属性字典
    f.__defaults__    :    默认参数元组
    ......
    

    我们在使用装饰器后,再使用上面这些属性访问时,看到的是内部包裹函数的元数据,原来函数的元数据丢失掉了,应该如何解决?

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

    我们新建一个函数,看看有哪些元数据

    In [1]: def f(a):
       ...:     '''f function'''
       ...:     return a*2
       ...:
    
    In [2]: f.__
       f.__annotations__  f.__delattr__      f.__ge__           f.__init__         f.__ne__           f.__setattr__
       f.__call__         f.__dict__         f.__get__          f.__kwdefaults__   f.__new__          f.__sizeof__
       f.__class__        f.__dir__          f.__getattribute__ f.__le__           f.__qualname__     f.__str__
       f.__closure__      f.__doc__          f.__globals__      f.__lt__           f.__reduce__       f.__subclasshook__
       f.__code__         f.__eq__           f.__gt__           f.__module__       f.__reduce_ex__
       f.__defaults__     f.__format__       f.__hash__         f.__name__         f.__repr__
    
    In [2]: f.__name__
    Out[2]: 'f'
    
    In [3]: g = f
    
    In [4]: g.__name__
    Out[4]: 'f'
    我们发现,结果和引用函数的变量名无关,只要是同一个对象
    
    In [5]: f.__doc__
    Out[5]: 'f function'
    函数所属哪个模块
    In [6]: f.__module__
    Out[6]: '__main__'
    

    再看下保存默认参数的defaults,在python中默认参数不是每次调用的时创建的,而是在定义函数的时候,直接创建好了。

    In [8]: def f(a,b=1,c=[]):
       ...:     print(a,b,c)
       ...:
    
    In [9]: f.__defaults__
    Out[9]: (1, [])
    
    我们在调用之前实现赋值给默认的list
    In [10]: f.__defaults__[1].append('abc')
    
    在调用之前,已经创建
    In [12]: f.__defaults__
    Out[12]: (1, ['abc'])
    
    In [11]: f(100)
    100 1 ['abc']
    

    在默认参数中,尽量不要使用可变变量。

    查看函数的包裹函数

    In [14]: def f():
        ...:     a = 2
        ...:     return lambda k : a ** k
        ...:
    
    In [15]: g = f()
    
    In [16]: g.__closure__
    Out[16]: (<cell at 0x000001C8209745E8: int object at 0x00000000771A01F0>,)
    
    In [17]: c = g.__closure__[0]
    
    In [18]: c.cell_contents
    Out[18]: 2
    

    当装饰器存在的时候,元数据变成了包裹函数的

    In [21]: def mydecorator(func):
        ...:     def wrapper(*args,**kargs):
        ...:         '''wrapper function'''
        ...:         print("In wrapper")
        ...:         func(*args,**kargs)
        ...:     return wrapper
        ...:
    
    In [22]: @mydecorator
        ...: def example():
        ...:     """example function"""
        ...:     print("In example")
        ...:
    
    In [23]: example.__name__
    Out[23]: 'wrapper'
    
    In [24]: example.__doc__
    Out[24]: 'wrapper function'
    

    我们使用wrapper模块

    In [25]: from functools import  update_wrapper,wraps
    
    In [26]: def mydecorator(func):
        ...:     def wrapper(*args,**kargs):
        ...:         '''wrapper function'''
        ...:         print("In wrapper")
        ...:         func(*args,**kargs)
        ...:     update_wrapper(wrapper,func,("__name__","__doc__"),("__dict__",))
        ...:     return wrapper
        ...:
        ...:
    
    In [27]: @mydecorator
        ...: def example():
        ...:     """example function"""
        ...:     print("In example")
        ...:
    
    In [28]: example.__name__
    Out[28]: 'example'
    
    In [29]: example.__doc__
    Out[29]: 'example function'
    

    通过后面个两个元组,我们指定不能被包裹函数覆盖的元数据。
    我们也可以使用默认参数。

    In [30]: from functools import  update_wrapper,wraps,WRAPPER_ASSIGNMENTS,WRAPPER_UPDATES
    
    In [31]: def mydecorator(func):
        ...:     def wrapper(*args,**kargs):
        ...:         '''wrapper function'''
        ...:         print("In wrapper")
        ...:         func(*args,**kargs)
        ...:     update_wrapper(wrapper,func)
        ...:     return wrapper
        ...:
        ...:
    
    In [32]: print(WRAPPER_ASSIGNMENTS)
    ('__module__', '__name__', '__qualname__', '__doc__', '__annotations__')
    
    In [33]: print(WRAPPER_UPDATES)
    ('__dict__',)
    

    更简便的方式是直接使用wraps函数装饰内部包裹函数

    In [34]: def mydecorator(func):
        ...:     @wraps(func)
        ...:     def wrapper(*args,**kargs):
        ...:         '''wrapper function'''
        ...:         print("In wrapper")
        ...:         func(*args,**kargs)
        ...:     #update_wrapper(wrapper,func)
        ...:     return wrapper
        ...:
        ...:
    
    In [35]: @mydecorator
        ...: def example():
        ...:     """example function"""
        ...:     print("In example")
        ...:
    
    In [36]: example.__name__
    Out[36]: 'example'
    
    In [37]: example.__doc__
    Out[37]: 'example function'
    

    问题三:如何定义带参数的装饰器?

    实现一个装饰器,它用来检查被装饰函数的参数类型。装饰器可以通过参数指明函数参数的类型,调用时如果检测出类型不匹配则抛出异常。

    @typeassert(str,int,int)
    def f(a,b,c):
        ......
    
    @typeassert(y=list)
    def g(x,y):
        ......
    

    解决方案:
    提取函数签名:inspect.signature()
    带参数的装饰器,也就是根据参数定制话一个装饰器,可以看成生产装饰器的工厂。每次调用typeassert,返回一个特定的装饰器,然后用它去装饰其它函数。

    我们使用signature获得参数

    In [1]: from inspect import  signature
    
    In [2]: def f(a,b,c=1):pass
    
    获取签名
    In [3]: sig = signature(f)
    
    获取参数字典
    In [4]: sig.parameters
    Out[4]:
    mappingproxy({'a': <Parameter "a">,
                  'b': <Parameter "b">,
                  'c': <Parameter "c=1">})
    
    In [6]: a = sig.parameters['a']
    
    In [7]: a.name
    Out[7]: 'a'
    
    In [8]: a.default
    Out[8]: inspect._empty
    
    In [9]: c = sig.parameters['c']
    
    In [10]: c.default
    Out[10]: 1
    
    我们实现参数类型和参数的绑定
    In [14]: sig.bind(str,int,int)
    Out[14]: <BoundArguments (a=<class 'str'>, b=<class 'int'>, c=<class 'int'>)>
    
    In [15]: bargs = sig.bind(str,int,int)
    
    生成对应字典
    In [16]: bargs.arguments
    Out[16]: OrderedDict([('a', str), ('b', int), ('c', int)])
    
    In [17]: bargs.arguments['a']
    Out[17]: str
    
    注意一点是,bind()函数要求全部参数复制,当缺少的时候,则会报错
    我们可以使用bind_partial()
    In [18]: sig.bind_partial(str)
    Out[18]: <BoundArguments (a=<class 'str'>)>
    会按顺序绑定
    
    

    下面我们写带参数的装饰器:

    In [13]: def typeassert(*ty_args,**ty_kargs):
        ...:     def decorator(func):
        ...:         # 这里获得函数参数和类型的关系 func -> a,b (获得函数签名)
        ...:         # d = {'a':int,'b':str} 通过参数获得关系映射的字典
        ...:         def wrapper(*args,**kargs):
        ...:             # 迭代参数,判断是否在d字典中  arg in d,instance(arg,d[arg])
        ...:             return func(*args,**kargs)
        ...:         return wrapper
        ...:     return decorator
        ...:
    

    现在我们将上面的框架补全:

    In [19]: def typeassert(*ty_args,**ty_kargs):
        ...:     def decorator(func):
        ...:         sig = signature(func)
        ...:         btypes = sig.bind_partial(*ty_args,**ty_kargs).arguments
        ...:         def wrapper(*args,**kargs):
        ...:             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
        ...:
    
    In [20]: @typeassert(int,str,list)
        ...: def f(a,b,c):
        ...:     print(a,b,c)
        ...:
    
    In [21]: f(1,'abc',[1,2,3])
    1 abc [1, 2, 3]
    
    In [22]: f(1,2,[1,2,3])
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-22-1c6f13da3d30> in <module>()
    ----> 1 f(1,2,[1,2,3])
    
    <ipython-input-19-299c61c8c1c1> in wrapper(*args, **kargs)
          7                 if name in btypes:
          8                     if  not isinstance(obj,btypes[name]):
    ----> 9                         raise TypeError('"%s" must be "%s"' % (name,btypes[name]))
         10             return func(*args,**kargs)
         11         return wrapper
    
    TypeError: "b" must be "<class 'str'>"
    

    这里有一点不明白 if name in btypes。

    如何实现属性可修改的函数装饰器?

    问题内容:
    为分析程序内有哪些函数执行时间开销较大,我们定义一个带timeout参数的函数装饰器。装饰器功能如下:
    1,统计被装饰函数单次调用运行时间
    2,时间大于参数timeout的,将此次函数调用记录到log日志中
    3,运行时可修改timeoout的值

    In [1]: from functools import  wraps
    
    In [2]: import time
    
    In [3]: import logging
    
    In [4]: def warn(timeout):
       ...:     def decorator(func):
       ...:         def wrapper(*args,**kargs):
       ...:             start = time.time()
       ...:             res = func(*args,**kargs)
       ...:             used = time.time() - start
       ...:             if used > timeout:
       ...:                 msg = '"%s":  %s > %s' % (func.__name__,used,timeout)
       ...:                 logging.warn(msg)
       ...:             return res
       ...:         return wrapper
       ...:     return decorator
       ...:
    
    In [5]: from random import  randint
    
    In [6]: @warn(1.5)
       ...: def test():
       ...:     print("In test")
       ...:     while randint(0,1):
       ...:         time.sleep(0.5)
       ...:
    
    In [7]: for _ in range(30):
       ...:     test()
       ...:
    In test
    In test
    In test
    C:\Users\wex\AppData\Local\Programs\Python\Python35\Scripts\ipython:9: DeprecationWarning: The 'warn' function is deprecated, use 'warning' instead
    WARNING:root:"test":  1.5016095638275146 > 1.5
    In test
    In test
    In test
    In test
    In test
    In test
    In test
    In test
    WARNING:root:"test":  1.5012586116790771 > 1.5
    In test
    In test
    WARNING:root:"test":  3.0051050186157227 > 1.5
    In test
    WARNING:root:"test":  2.0030357837677 > 1.5
    In test
    In test
    In test
    In test
    In test
    In test
    In test
    WARNING:root:"test":  1.5070884227752686 > 1.5
    In test
    WARNING:root:"test":  2.003775119781494 > 1.5
    In test
    In test
    In test
    In test
    In test
    In test
    In test
    In test
    

    相关文章

      网友评论

        本文标题:python进阶:第八章(装饰器使用技巧)

        本文链接:https://www.haomeiwen.com/subject/psrctxtx.html