| |
|
|
开发:
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图书馆 购物 三丰科技 阅读网 日历 万年历 2025年12日历 | -2025/12/3 0:58:57- |
|
| 网站联系: qq:121756557 email:121756557@qq.com IT数码 |