装饰器
能够在不改变原有函数或类的基础上,在原来的基础上添加额外的功能的代码,就叫做装饰器
由来
需求:为一个加法函数增加记录实参的功能
装饰器语法
@logger
就是装饰器语法- 答疑一: fn和ret是什么?
- fn为装饰器函数形参, 接收被装饰函数作为的实参, 故fn指向的即为别装饰函数
- ret是被装饰函数的返回值. 如果被装饰函数有返回值, 则在功能增强后被返回, 如果被装饰函数没有返回值, 则ret指向的为None, 返回None.(一般情况, 函数没有返回值, 隐性返回None)
- 答疑二: 被装饰函数为定长形参列表, 为什么在装饰器函数代码中fn(*args, **kwargs)写成这样?
- 通用写法, 当被装饰函数形参列表为定长时, 也可以这么写, 同时如果被装饰函数为不定长参数列表的函数, 同样适用. 兼容性更强.
无参装饰器
- 上面例子的装饰器语法,称之为无惨装饰器
- @符号后是一个函数
- 虽然是无参装饰器,但是@后的函数本质上是单参函数
- 上例的logger函数是一个高阶函数
日志记录装饰器实现
装饰器本质
- 习惯上add函数被称为被包装函数wrapped,增强它的函数称为包装器、包装函数wrapper
- 被包装函数就如有一幅画,包装它的目的是为了增强,而不是破坏画,采用非侵入式代码
- 可以在原画前或后加入增强代码
- 装饰器如同画框,装饰器可以更换,如同更换画框,画不变。也可以画框不变,更换画。也可以画框外再加其他装饰器。
带参装饰器
文档字符串
- Python文档字符串Documentation Strings
- 在函数(类、模块)语句块的第一行,且习惯是多行文本,所以多使用三引号
- 文档字符串也算是合法的一条语句
- 惯例是首字母大写,第一行写概述,空一行,第三行写详细描述
- 可以使用特殊属性__doc__访问这个文档
装饰器的文档字符串问题
在上一节无参装饰器的装饰器语法例子中,
print(add.__name__, add.__doc__)
语句执行后,发现add函数的函数名和文档都变了。如何解决?函数也是对象,特殊属性也是属性,也可以被覆盖。现在访问add函数实际上是wrapper函数,所以使用原定义的add函数的名称和文档属性覆盖wrapper的对应属性就可以了。
如果copy_properties是公用的函数,可以定义成全局的。实际上,这个函数很通用,基本上装饰器都会用这个问题
带参装饰器
能否把copy_properties改成装饰器?这个装饰器是带参装饰器。
像
@ copy_properties(fn)
这种在装饰器后面跟着参数的装饰器称为带参装饰器对logger设定一个阈值,对执行时长超出阈值的记录一下。
(对logger函数进一步柯里化,添加参数,logger(a, b ,c ,fn)→ logger(a, b ,c)(fn))
logger函数中,添加多个形参的处理方式
进一步提取记录功能,因为有可能输出到控制台,也可能写入日志,这个由一个函数提供。
属性更新
上面例子中copy_properties是通用的功能,标准库中functools已经提供了
functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WARPPER_UPDATES)
- 类似copy_properties功能
- wrapper包装函数、被更新者,wrapped被包装函数、数据源
- 元组WRAPPER_ASSIGNMENTS中是要被覆盖的属性,由模块名
__module__
,名称__name__
,限定名__qualname__
,文档__doc__
,参数注解__annotations__
- 元组WRAPPER_UPDATES中是要被更新的属性,
__dic__
属性字典
- 增加一个
__wrapped__
属性,保留着wrapped函数
。。。。。
总结
- @之后不是一个单独的标识符,是一个函数调用
- 函数调用的返回值又是一个函数,此函数是一个无参装饰器
- 带参装饰器,可以有任意个参数
- @func()
- @func(1)
- @func(1, 2)