| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> Python知识库 -> Python 函数装饰器和闭包 -> 正文阅读 |
|
[Python知识库]Python 函数装饰器和闭包 |
函数装饰器和闭包函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为,用闭包实现。 nolocal是在Python3.0新增的保留关键字。 闭包除了在装饰器中有用之外,还回调式一步编程和函数式编程风格的基础。 装饰器基础,何时执行装饰器装饰器是可调用对象,参数是另一个函数(被装饰的函数)。 函数装饰器再导入模块是立即执行,而被装饰的函数只有在调用时运行。 装饰器通常在一个模块中定义,然后应用到其他模块中的函数上。 registry = [] def register(func): print('running :{}'.format(func)) registry.append(func) return func @register def f1(): print('f1 running.') @register def f2(): print('f2 running.') def f3(): print('f3 running.') def main(): print(registry) f1() f2() f3() if __name__ == '__main__': main() 打印 running :<function f1 at 0x034F9618> running :<function f2 at 0x034F96A8> [<function f1 at 0x034F9618>, <function f2 at 0x034F96A8>] f1 running. f2 running. f3 running. 以上可以看到,在加载模块时,函数装饰器立即执行。被装饰的函数在调用时执行。 示例中的装饰器原封不动的返回被装饰的函数,这种并不是没有用途,比如在Python web框架中,这样的装饰器把函数添加到某种中央注册处,实现路由功能。 使用装饰器实现“策略”模式使用装饰器,这种方案有几个优点:
示例,使用装饰器完成策略模式(部分代码) promos = [] def promotion(func): promos.append(func) return func @promotion def FidelityPromo(order): """具体策略:有1000积分以上的顾客,整个订单可以享受5%的折扣""" return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0 @promotion def BulkItemPromo(order): """具体策略:同一个订单中,单个商品的数量达到20个以上,单品享受10%折扣""" discount = 0 for item in order.cart: if item.quantity >= 20: discount += item.total() * .1 return discount @promotion def LargeOrderPromo(order): """具体策略:订单中不同商品的数量达到10个以上,整个订单享受7%折扣""" return order.total() * 0.07 if len({item.product for item in order.cart}) >= 10 else 0 def best_promo(oder): """选择最佳的折扣""" return max(promo(oder) for promo in promos) 变量作用域这个是一个让人吃惊的示例 b = 2 def f(a): print(a) print(b) b = 2 f(1) 打印 1 Traceback (most recent call last): File "C:/Users/lijiachang/PycharmProjects/collect_demo/test2.py", line 53, in <module> f(1) File "C:/Users/lijiachang/PycharmProjects/collect_demo/test2.py", line 49, in f print(b) UnboundLocalError: local variable 'b' referenced before assignment 如果在函数内不加b = 2 程序可以正常执行,因为b会找到全局变量中的b=2 但是在函数最后加上了b=2,在Python编译函数的定义体时,判断出b是局部变量,因为在函数内给它赋值了,就不会获取全局变量。 比较字节码来论证: dis模块为反汇编Python函数字节码提供了简单的方式。 def f2(a): print(a) print(b) from dis import dis print(dis(f2)) 结果可以看到a为LOAD_FAST 本地名称,b为LOAD_GLOBAL全局名称。 b = 2 def f2(a): print(a) print(b) b =2 from dis import dis print(dis(f2)) 可以看到这次b是LOAD_FAST 本地名称 另外补充: LOAD_FAST(var_num) 将对本地co_varnames [var_num]的引用压入堆栈。 闭包假如有个avg函数,他的作用是计算不断增加系列值的均值。 示例,使用类实现 class Avg: def __init__(self): self.elements = [] def __call__(self, *args, **kwargs): self.elements.append(args[0]) return sum(self.elements) / len(self.elements) avg = Avg() print(avg(10)) print(avg(11)) print(avg(12)) 打印 10.0 10.5 11.0 示例,使用闭包实现 def make_avg(): elements = [] def inner(element): elements.append(element) return sum(elements) / len(elements) return inner avg = make_avg() print(avg(10)) print(avg(11)) print(avg(12)) 打印 10.0 10.5 11.0 Avg类的实例avg存储历史值是在self.elements实例属性中。 在inner函数中,elements是自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量 我们可以使用函数的__code__属性中查询局部变量和自由变量的名称 __code__.co_varnames 局部变量 __code__.co_freevars 自由变量 avg.__closure__[0].cell_contents 自由变量绑定的真实值 avg = make_avg() avg(10) print(avg.__code__.co_varnames) # 局部变量 print(avg.__code__.co_freevars) # 自由变量 print(avg.__closure__) # 自由变量绑定的值 print(avg.__closure__[0].cell_contents) # 自由变量绑定的真正的值 打印 ('element',) ('elements',) (<cell at 0x02CEEF50: list object at 0x02CA4A80>,) [10] 从上所述,闭包是一种函数,它会保留定义函数时,存在自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。 nonlocal声明前面的make_avg效率不高,应该只存储目前的总值和元素个数。 def make_avg(): total = 0 # 总值 count = 0 # 元素个数 def inner(element): nonlocal total, count total += element count += 1 return total / count return inner 首先说明区别,total和count是数字,对于数字、字符串、元祖等不可变类型,都是只读,可能更新。 而之前的例子中的elements是列表,是可变的对象。 这里就需要用到了nonlocal这个关键字,因为count+=1这样就在inner定义体中未count赋值了,这样会把count变成局部变量。 如果尝试count+=1,其实会隐式的创建局部变量count,这样count就不是自由变量了,不会保存到闭包中。 在Python3中,引入了nonlocal声明。它的作用是标记变量为自由变量,即使在函数中为变量赋予了新值,也是自由变量,如果赋予了新值,闭包中保存的绑定也会更新。 在Python2中,需要变通的办法,可以把内部函数修改的变量存储为可变对象(比如字典或者简单的实例),并且把那个对象绑定为一个自由对象。 简单的装饰器-记录函数运行时间函数计时,记录经过的时间、传入的参数、调用的结果打印出来 import time import functools def clock(func): """函数执行时间,参数,结果""" @functools.wraps(func) def inner(*args, **kwargs): t0 = time.time() result = func(*args, **kwargs) time_use = time.time() - t0 func_name = func.__name__ parameter = [] if args: parameter.append(','.join(repr(arg) for arg in args)) if kwargs: kwargs_str = ",".join('%s=%r' % (k, v) for k, v in kwargs.items()) parameter.append(kwargs_str) parameter = ','.join(parameter) print('[%0.8fs] %s(%s) -> %r' % (time_use, func_name, parameter, result)) return result return inner @clock def use(a, b, c=1): time.sleep(1) print(a, b, c) @clock def fab(n): return 1 if n < 2 else fab(n - 1) * n use(4, '5', c=6) fab(6) 打印 4 5 6 [1.00087142s] use(4,'5',c=6) -> None [0.00000000s] fab(1) -> 1 [0.00000000s] fab(2) -> 2 [0.00000000s] fab(3) -> 6 [0.00000000s] fab(4) -> 24 [0.00000000s] fab(5) -> 120 [0.00000000s] fab(6) -> 720 示例中要注意的几点: @functools.wraps(func) 还原被装饰函数的 __name__和__doc__属性。 %0.8f 表示小数点后保留8位小数 %r 表示适合机器阅读的格式,相对于%s好处是可以接受数字类型。而%s接受到数字会报错。%r接受到字符串时会添加单引号'' 标准库中的装饰器functools.lru_cache实现缓存功能。这是一项优化技术,把耗时的函数的结果保存起来,避免了传入相同的参数时重复计算。 lru是Least Recently Used的缩写,表明缓存不会无限制增长,一段时间不用的缓存条目会被扔掉。 生成第n个斐波那契数列,这种慢速递归函数非常适合使用lru_cache。 ?斐波那契数列指的是这样一个数列 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 示例,生成第n个斐波那契数列 import time import functools def clock(func): """函数执行时间,参数,结果""" @functools.wraps(func) def inner(*args, **kwargs): t0 = time.time() result = func(*args, **kwargs) time_use = time.time() - t0 func_name = func.__name__ parameter = [] if args: parameter.append(','.join(repr(arg) for arg in args)) if kwargs: kwargs_str = ",".join('%s=%r' % (k, v) for k, v in kwargs.items()) parameter.append(kwargs_str) parameter = ','.join(parameter) print('[%0.8fs] %s(%s) -> %r' % (time_use, func_name, parameter, result)) return result return inner @clock def fibonacci(n): """计算斐波那契数列的第n个项(从0项开始)""" if n < 2: return n return fibonacci(n - 2) + fibonacci(n - 1) print(fibonacci(4)) 打印 [0.00000000s] fibonacci(0) -> 0 [0.00000000s] fibonacci(1) -> 1 [0.00000000s] fibonacci(2) -> 1 [0.00000000s] fibonacci(1) -> 1 [0.00000000s] fibonacci(0) -> 0 [0.00000000s] fibonacci(1) -> 1 [0.00000000s] fibonacci(2) -> 1 [0.00100422s] fibonacci(3) -> 2 [0.00100422s] fibonacci(4) -> 3 3 可以看到,浪费时间的地方很明显,fibonacci(1)的运算调用了三次,如果使用lru_cache装饰后,性能会显著改善。 @functools.lru_cache() @clock def fibonacci(n): """计算斐波那契数列的第n个项(从0项开始)""" if n < 2: return n return fibonacci(n - 2) + fibonacci(n - 1) print(fibonacci(4)) 打印 [0.00000000s] fibonacci(0) -> 0 [0.00000000s] fibonacci(1) -> 1 [0.00000000s] fibonacci(2) -> 1 [0.00000000s] fibonacci(3) -> 2 [0.00000000s] fibonacci(4) -> 3 3 每个n值值调用了一次函数,因为重复的调用会在缓存中取,不再真实的调用。 lru_cache可以使用两个可选的参数。lru_cache(maxsize=128, typed=False) maxsize 表示存储多少个调用的结果。 typed 如果设置为True,会把不同的类型分开存储,比如说通常认为1.0和1是一样的结果,但是他们类型不同,一个是浮点数,一个数整数。 ps:因为lru_cache使用字典存储结果,键值是根据调用时传入的参数来创建的,所有被装饰的函数的参数,必须是可散列的。 单分派泛函数 singledispatchPython3.4中新增的functools.singledispathch装饰器,可以把整体方案拆分成多个模块,还可以为无法修改的类提供专门的函数。 Python2.6可以使用第三方模块singledispatch实现。 使用@singledispathch装饰的普通函数会变成泛函数: 根据第一个参数的类型,以不同的方式执行相同的操作的一种函数。 如果是根据多个参数选择专门的函数,就是多分派了。 示例,一个简单的生产HTML的工具 import html def htmlize(obj): content = html.escape(repr(obj)) return '<pre>%s</pre>' % content print(htmlize({1, 2, 3})) print(htmlize(abs)) 打印 <pre>{1, 2, 3}</pre> <pre><built-in function abs></pre> 扩展: html.escape 函数是把字符串 转换成 HTML转义符 html.unescape 函数是把HTML转义符再换成字符串 (这俩函数常用于爬虫) 比如上面的<转成了< >转成了> 现在想对这个htmlize函数做一个扩展,根据参数类型的不同,进行不同的方式处理。通常是htmlize函数变成一个分派函数,里面使用不同的if elif来判断不同类型,调用其他函数。这样并不优雅,使用functools.singledispatch能完成这个方案。 当使用@singledispatch装饰器,装饰了htmlize函数,就可以创建出一个htmlize.register()装饰器,使用这个装饰器就可以把多个函数绑定一起组成一个泛函数。
from functools import singledispatch from collections import abc import html import numbers @singledispatch def htmlize(obj): content = html.escape(repr(obj)) return '<pre>%s</pre>' % content @htmlize.register(str) def _(text): """如果是str类型:把内部换行符\n改为<br>\n 不使用<pre> 而是使用<p>""" content = html.escape(text).replace(r'\n', r'<br>\n') return '<p>%s</p>' % content @htmlize.register(numbers.Integral) # numbers.Integral 是int的虚拟超类 def _(number): """如果是int类型:以十进制和十六进制显示数字""" return '<pre>%d (0x%x)</pre>' % (number, number) # %x以十六进制显示,不带0x @htmlize.register(tuple) @htmlize.register(abc.MutableSequence) # 可变序列MutableSequence ,继承自序列Sequence def _(seq): """如果是tuple或者可变序列,输出一个HTML列表,根据各个元素的类型格式化""" res = [] for element in seq: res.append('<li>{}</li>'.format(htmlize(element))) return '<ul>\n{}\n</ul>'.format('\n'.join(res)) print(htmlize(r'wo \n ai')) print(htmlize(42)) print(htmlize(['wo', 66, {1, 2, 3}])) 打印 <p>wo <br>\n ai</p> <pre>42 (0x2a)</pre> <ul> <li><p>wo</p></li> <li><pre>66 (0x42)</pre></li> <li><pre>{1, 2, 3}</pre></li> </ul> 扩展: abc.MutableSequence 代表任何可变序列,集成自序列Sequence numbers.Integral 是int的虚拟超类 %x 可以让数字以十六进制显示,但是不带0x 注册的专门函数的参数类型,应该尽量使用像abc.MutableSequence和numbers.Integral这种抽象基类,这样代码支持的兼容类型就更广泛。 补充,在Python3.8版本中带来了: singledispatchmethod In : class Dispatch: ...: @singledispatch ...: def foo(self, a): ...: return a ...: ...: @foo.register(int) ...: def _(self, a): ...: return 'int' ...: ...: @foo.register(str) ...: def _(self, a): ...: return 'str' ...: In : cls = Dispatch() In : cls.foo(1) Out: 1 # 没有返回 'int' In : cls.foo('s') Out: 's' # 没有返回 'str'
>>> from functools import singledispatchmethod >>> class Dispatch: ... @singledispatchmethod ... def foo(self, a): ... return a ... ... @foo.register(int) ... def _(self, a): ... return 'int' ... ... @foo.register(str) ... def _(self, a): ... return 'str' ... >>> cls = Dispatch() >>> cls.foo(1) 'int' >>> cls.foo('s') 'str' 参数化装饰器想要实现装饰器接收参数,需要创建一个装饰器工厂函数,把参数传给他,返回一个装饰器,然后再把它用在要装饰去函数上。 一个参数化的装饰器,让register装饰器具备可选注册和注销功能。通过设置一个参数,设为FALSE时,不注册被装饰去函数。 注意此时register函数不是装饰器,而是装饰器工厂函数。调用它时才会返回真正的装饰器。 示例,接收参数的注册功能装饰器 registry = set() # 使用集合,添加和删除速度更快 def register(active=True): def decorate(func): # decorate才是装饰器 if active: # 通过闭包获取active的值,是True时才注册 registry.add(func) else: registry.discard(func) return func return decorate @register(active=False) def f1(): print('f1 running.') @register() def f2(): print('f2 running.') def f3(): print('f3 running.') print('main running') print('registry ->', registry) 打印 main running registry -> {<function f2 at 0x037C9738>} 扩展: 使用set集合比list添加和删除的速度更快 set.discard(x) 删除指定元素,set.? 以上可以看到只有f2函数被注册了,f1虽然被装饰了,但是active=False,所以f1不会被注册。 如果不使用@语法,像常规函数那样使用register,装饰f2函数就是register()(f2),装饰f1函数就是register(active=False)(f1) 一个完整的三层函数的装饰器 最外层的函数是装饰器的工厂函数。第二层才是真正的装饰器,第三层是用来包装被装饰的函数用的。 示例,一个clock装饰器,参数是格式字符串。 """clockdeco_param.py""" import time import functools FMT = '[{time_use:0.8f}s] {func_name}({parameter}) -> {result}' # 格式化的模板 (作为装饰器参数) def clock(fmt=FMT): """函数执行时间,参数,结果""" def decorate(func): @functools.wraps(func) def inner(*args, **kwargs): t0 = time.time() result = func(*args, **kwargs) time_use = time.time() - t0 func_name = func.__name__ parameter = [] if args: parameter.append(','.join(repr(arg) for arg in args)) if kwargs: kwargs_str = ",".join('%s=%r' % (k, v) for k, v in kwargs.items()) parameter.append(kwargs_str) parameter = ','.join(parameter) # print('[%0.8fs] %s(%s) -> %r' % (time_use, func_name, parameter, result)) print(fmt.format(**locals())) # 这里拆包局部变量,省去了参数字典 return result return inner return decorate @clock() def test(s, a=2): time.sleep(1) print('func test running') return 'over' test('wo', a=3) 打印 func test running [1.01234961s] test('wo',a=3) -> over clock是参数化装饰器的工厂函数,decorate才是真正的装饰器。 扩展: 代码中使用了locals()然后拆包,locals()里面保存了inner函数的局部变量信息,是个字典,如果打印出来就是: {'args': ('wo',), 'kwargs': {'a': 3}, 't0': 1618408087.1784744, 'result': 'over', 'time_use': 1.0123496055603027, 'func_name': 'test', 'parameter': "'wo',a=3", 'kwargs_str': 'a=3', 'fmt': '[{time_use:0.8f}s] {func_name}({parameter}) -> {result}', 'func': <function test at 0x020F4150>} 如果在其他文件中使用clockdeco_param.py模块中的clock装饰器,可以: from?clockdeco_param import clock @clock('{func_name}: {time_use}s') def func():pass 这样就可以随意改变输出格式: func: 0s 能接受可选参数的装饰器那之前的注册装饰器来说,要实现可选参数,也就是可以使用@register、@register()、@register(active=True)这三种形式 这三种形式如果不用@表示,使用函数表示,分别是register(f)、 register()(f)、 register(active=True)(f) 这样就比较好理解以下的代码了 registry = set() # 使用集合,添加和删除速度更快 def register(f=None, active=True): if f is not None: registry.add(f) def decorate(func): # decorate才是装饰器 if active: # 通过闭包获取active的值,是True时才注册 registry.add(func) else: registry.discard(func) return func return decorate @register(active=False) def f1(): print('f1 running.') @register() def f2(): print('f2 running.') @register def f3(): print('f3 running.') print('main running') print('registry ->', registry) 打印结果 main running registry -> {<function f3 at 0x02C49618>, <function f2 at 0x02C49738>} 可以看到register装饰器加不加括号()都成功把函数注册成功了。 ps:思路来自《Python Cookbook》 |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/15 17:34:18- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |