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

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

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

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,600评论 18 139
  • 7.1 创建装饰器 与java中的装饰器模式类似,其作用就是将一些多余的、能够重复使用的代码抽离出来,然后通过py...
    Lemon_Home阅读 496评论 0 0
  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,146评论 9 118
  • 本文为《爬着学Python》系列第十篇文章。 在实际操作中,可能函数是我们几乎唯一的实现操作的方式,这是因为函数能...
    SyPy阅读 5,446评论 0 8
  • 画牡丹前,挤出四种基础颜料-藤黄、赭石、花青与 和曙红。 起笔水份饱满些,笔尖端部分调入曙红,藏锋画起。 根据牡丹...
    镜子静阅读 590评论 0 0