Lesson 029 —— python 修饰器
Lesson 029 —— python 修饰器
本质就是函数,功能是为其它函数添加附加功能。
原则:
- 不修改被修饰函数的源代码
- 不修改被修饰函数的调用方式
装饰器 = 高阶函数 + 函数嵌套 + 闭包
高阶函数
高阶函数英文叫Higher-order function。什么是高阶函数?
以Python内置的求绝对值的函数abs()
为例,abs(-10)
是函数调用,而abs
是函数本身。函数本身也可以赋值给变量,即:变量可以指向函数。变量f
现在已经指向了abs
函数本身。直接调用abs()
函数和调用变量f()
完全相同。
>>> f = abs
>>> f
<built-in function abs>
>>> f = abs
>>> f(-10)
10
那么函数名是什么呢?函数名其实就是指向函数的变量!对于abs()
这个函数,完全可以把函数名abs
看成变量,它指向一个可以计算绝对值的函数!
>>> abs = 10
>>> abs(-10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable
把abs
指向10
后,就无法通过abs(-10)
调用该函数了!因为abs
这个变量已经不指向求绝对值函数而是指向一个整数10
!
当然实际代码绝对不能这么写,这里是为了说明函数名也是变量。要恢复abs
函数,请重启Python交互环境。
注:由于abs
函数实际上是定义在import builtins
模块中的,所以要让修改abs
变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10
。
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
函数嵌套
在函数中定义另一个函数称为嵌套函数。嵌套函数可以访问包围范围内的变量。
在Python中,这些非局部变量只能在默认情况下读取,我们必须将它们显式地声明为非局部变量(使用nonlocal
关键字)才能进行修改。
# This is the outer enclosing function
def outer(msg):
# This is the nested function
def inner():
print('inner')
print(msg)
print('outer')
inner()
outer(‘hello’)
inner(‘hello’) # 此句会出错
函数有可见范围,这就是作用域的概念
内部函数不能被外部直接使用,会抛NameError异常
可以看到嵌套函数inner()
能够访问封闭函数的非局部变量msg
闭包
闭包是由函数及其相关的引用环境组合而成的实体(即:闭包=函数+引用环境)
python中的闭包从表现形式上定义(解释)为:如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure).
- 必须有一个嵌套函数(函数内部的函数)。
- 嵌套函数必须引用封闭函数中定义的值。
- 闭包函数必须返回嵌套函数。
>>>def addx(x):
>>> def adder(y): return x + y
>>> return adder
>>> c = addx(8)
>>> type(c)
<type 'function'>
>>> c.__name__
'adder'
>>> c(10)
18
结合这段简单的代码和定义来说明闭包:
- 如果在一个内部函数里:adder(y)就是这个内部函数,
- 对在外部作用域(但不是在全局作用域)的变量进行引用:x就是被引用的变量,x在外部作用域addx里面,但不在全局作用域里,
- 则这个内部函数adder就是一个闭包。
再稍微讲究一点的解释是,闭包=函数块+定义函数时的环境,adder就是函数块,x就是环境,当然这个环境可以有很多,不止一个简单的x。
注意事项:
-
闭包中是不能修改外部作用域的局部变量的
>>> def foo(): ... m = 0 ... def foo1(): ... m = 1 ... print m ... ... print m ... foo1() ... print m ... >>> foo() 0 1 0
从执行结果可以看出,虽然在闭包里面也定义了一个变量m,但是其不会改变外部函数中的局部变量m。
def foo(): a = 1 def bar(): a = a + 1 return a return bar # 结果 >>> c = foo() >>> print c() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in bar UnboundLocalError: local variable 'a' referenced before assignment
这是因为在执行代码 c = foo()时,python会导入全部的闭包函数体bar()来分析其的局部变量,python规则指定所有在赋值语句左面的变量都是局部变量,则在闭包bar()中,变量a在赋值符号"="的左面,被python认为是bar()中的局部变量。再接下来执行print c()时,程序运行至a = a + 1时,因为先前已经把a归为bar()中的局部变量,所以python会在bar()中去找在赋值语句右面的a的值,结果找不到,就会报错。解决的方法很简单
def foo(): a = [1] def bar(): a[0] = a[0] + 1 return a[0] return bar
只要将a设定为一个容器就可以了。这样使用起来多少有点不爽,所以在python3以后,在a = a + 1 之前,使用语句nonloacal a就可以了,该语句显式的指定a不是闭包的局部变量。
-
还有一个容易产生错误的事例也经常被人在介绍python闭包时提起,我一直都没觉得这个错误和闭包有什么太大的关系,但是它倒是的确是在python函数式编程是容易犯的一个错误,我在这里也不妨介绍一下。先看下面这段代码
for i in range(3): print i
在程序里面经常会出现这类的循环语句,Python的问题就在于,当循环结束以后,循环体中的临时变量i不会销毁,而是继续存在于执行环境中。还有一个python的现象是,python的函数只有在执行时,才会去找函数体里的变量的值。
flist = [] for i in range(3): def foo(x): print x + i flist.append(foo) for f in flist: f(2)
可能有些人认为这段代码的执行结果应该是2,3,4.但是实际的结果是4,4,4。这是因为当把函数加入flist列表里时,python还没有给i赋值,只有当执行时,再去找i的值是什么,这时在第一个for循环结束以后,i的值是2,所以以上代码的执行结果是4,4,4.
解决方法也很简单,改写一下函数的定义就可以了。for i in range(3): def foo(x,y=i): print x + y flist.append(foo)
作用:
-
当闭包执行完后,仍然能够保持住当前的运行环境。(当函数中有东西外边还有引用指向它的时候,它并不会立即回收,而是保存了这个函数的空间)
比如说,如果你希望函数的每次执行结果,都是基于这个函数上次的运行结果。我以一个类似棋盘游戏的例子来说明。假设棋盘大小为50*50,左上角为坐标系原点(0,0),我需要一个函数,接收2个参数,分别为方向(direction),步长(step),该函数控制棋子的运动。棋子运动的新的坐标除了依赖于方向和步长以外,当然还要根据原来所处的坐标点,用闭包就可以保持住这个棋子原来所处的坐标。
origin = [0, 0] # 坐标系统原点 legal_x = [0, 50] # x轴方向的合法坐标 legal_y = [0, 50] # y轴方向的合法坐标 def create(pos=origin): def player(direction,step): # 这里应该首先判断参数direction,step的合法性,比如direction不能斜着走,step不能为负等 # 然后还要对新生成的x,y坐标的合法性进行判断处理,这里主要是想介绍闭包,就不详细写了。 new_x = pos[0] + direction[0]*step new_y = pos[1] + direction[1]*step pos[0] = new_x pos[1] = new_y #注意!此处不能写成 pos = [new_x, new_y],原因在上文有说过 return pos return player player = create() # 创建棋子player,起点为原点 print(player([1,0],10)) # 向x轴正方向移动10步 print(player([0,1],20)) # 向y轴正方向移动20步 print(player([-1,0],10)) # 向x轴负方向移动10步
输出为
[10, 0] [10, 20] [0, 20]
-
闭包可以根据外部作用域的局部变量来得到不同的结果,这有点像一种类似配置功能的作用,我们可以修改外部的变量,闭包根据这个变量展现出不同的功能。比如有时我们需要对某些文件的特殊行进行分析,先要提取出这些特殊行。
def make_filter(keep): def the_filter(file_name): file = open(file_name) lines = file.readlines() file.close() filter_doc = [i for i in lines if keep in i] return filter_doc return the_filter
如果我们需要取得文件"result.txt"中含有"pass"关键字的行,则可以这样使用例子程序
filter = make_filter("pass") filter_result = filter("result.txt")
以上两种使用场景,用面向对象也是可以很简单的实现的,但是在用Python进行函数式编程时,闭包对数据的持久化以及按配置产生不同的功能,是很有帮助的。
装饰器(Decorator)
装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能.装饰器的作用就是为已经存在的对象添加额外的功能。
实际上,实现特殊方法__call__()
的任何对象都被称为可调用。 因此,在最基本的意义上,装饰器是可调用的,并且可以返回可调用。基本上,装饰器接收一个函数,添加一些函数并返回,这个新的函数一般不会修改被修饰函数的返回结果。
def make_pretty(func):
def inner():
print("I got decorated")
func()
return inner
def ordinary():
print("I am ordinary")
在 shell 中执行
>>> ordinary()
I am ordinary
>>> # let's decorate this ordinary function
>>> pretty = make_pretty(ordinary)
>>> pretty()
I got decorated
I am ordinary
在上面的例子中,make_pretty()
是一个装饰器。 在分配步骤。
pretty = make_pretty(ordinary)
函数ordinary()
得到了装饰,返回函数的名字:pretty
。
可以看到装饰函数为原始函数添加了一些新功能。这类似于包装礼物。 装饰器作为包装纸。 装饰物品的性质(里面的实际礼物)不会改变。 但现在看起来很漂亮(因为装饰了)。
一般来说,我们装饰一个函数并重新分配它,
ordinary = make_pretty(ordinary)
这是一个常见的结构,Python有一个简化的语法。
可以使用@
符号和装饰器函数的名称,并将其放在要装饰的函数的定义之上。 例如,
@make_pretty
def ordinary():
print("I am ordinary")
上面代码相当于:
def ordinary():
print("I am ordinary")
ordinary = make_pretty(ordinary)
注:如果 make_pretty
不加参数的话,比如直接 def make_pretty()
,这样定义,他是会报错的
@make_pretty
TypeError: make_pretty() takes 0 positional arguments but 1 was given
装饰器执行时机
在模块加载时就会执行修饰器函数, 生成一个个被修饰的新的函数. 这两点可简单验证.
# 模块 test.py
def w1(func):
print("---正在装饰1----")
def inner():
print("---1111111111----")
func()
print("---1111111111----")
return inner
@w1
def f1():
print("---f1---")
if __name__ == "__main__":
print("f1: ", f1)
print("w1: ", w1)
print("执行 f1()")
print(f1())
执行脚本:
>>> import test
---正在装饰1----
# 或者
python -c 'import test'
# 结果
---正在装饰1----
# 运行 test.py
python test.py
# 结果
---正在装饰1----
f1: <function w1.<locals>.inner at 0x7fad1bc22bf8>
w1: <function w1 at 0x7fad1bd091e0>
执行 f1()
---1111111111----
---f1---
---1111111111----
None
可以看出,装饰器 @w1
这一行,其实在函数没有被调用之前,即导入或者说加载的时候已经执行了, 这一句就等于 f1=w1(f1)
所以 w1
函数已经被调用了,返回的是inner()
函数的引用,
所以说如果再调用 f1()
,其实执行的是 inner()
,而真正的 f1()
函数的引用现在正被保存在 w1()
函数中的 func
参数里面。
被修饰后的函数已经不是原来的函数了.实际上, f1=w1(f1)
. 这样带来的坏处时屏蔽了原函数的元信息, 例如__name__, __doc__
. 如果要保留原函数的信息, 可使用标准库中的修饰器functools.wraps()
:
import functools
def w1(func):
print("---正在装饰1----")
@functools.wraps(func)
def inner():
print("---1111111111----")
func()
print("---1111111111----")
return inner
@w1
def f1():
print("---f1---")
if __name__ == "__main__":
print("f1: ", f1)
print("w1: ", w1)
print("执行 f1()")
print(f1())
# 结果
---正在装饰1----
f1: <function f1 at 0x7fed93231620>
w1: <function w1 at 0x7fed932ef1e0>
执行 f1()
---1111111111----
---f1---
---1111111111----
None
两层装饰(链接装饰器)
多个装饰器可以在Python中链接。
这就是说,一个函数可以用不同(或相同)装饰器多次装饰。只需将装饰器放置在所需函数之上。
def w1(func):
print("---正在装饰1----")
def inner():
print("---1111111111----")
func()
print("---1111111111----")
return inner
def w2(func):
print("---正在装饰2----")
def inner():
print("---2222222222----")
func()
print("---2222222222----")
return inner
@w1
@w2
def f1():
print("---f1---")
if __name__ == "__main__":
print("f1: ", f1)
print("w2: ", w2)
print("w1: ", w1)
print("执行 f1()")
print(f1())
从运行结果可以看出,首先调用装饰器 w2
,再调用装饰器 w1
,也就是说 运行到 @w1
这一行,因为在它下面的并不是一个函数,所以 w1
先暂停,先调用 w2
, w2
装饰完成之后,返回的是w2 的 inner 函数的引用, w1
再开始对 w2
的 inner
函数进行装饰. 最后返回的是w1 的 inner 函数.如果最后调用 f1() 那么运行结果为:
---正在装饰2----
---正在装饰1----
f1: <function w1.<locals>.inner at 0x7fd6af354d08>
w2: <function w2 at 0x7fd6af354b70>
w1: <function w1 at 0x7fd6af43b1e0>
执行 f1()
---1111111111----
---2222222222----
---f1---
---2222222222----
---1111111111----
None
实际的运行过程大概如下:
-
运行到
@w1
,它下面不是一个函数,w1
先暂停。 -
继续运行下一行
@w2
,符合条件,则执行w2()
,相当于f1 = w2(f1)
,w2
返回w2
中的inner
函数的引用,则可以看成f1 = w2.inner
。 -
@w2
执行完之后,检查@w1
,下面是函数,即上面的f1
,所以继续执行第一步暂停的w1
,即f1 = w1(f1) = w1(w2.inner)
,w1
返回w1
函数中的嵌套函数inner
的引用,则结果可以看成是f1 = w1.inner(w2.inner(f1))
(实际上inner
函数没有传入参数,是以闭包的性质传入的参数,即参数是从函数w1, w2
传入的),则f1
是w1
的ininer
函数的引用。 -
执行
f1
实际上就相当于执行w1.inner(w2.inner(f1))
,则按顺序执行;1, 先打印 ---1111111111---- 2. 然后执行传入的参数,即传入的函数;由于传入的参数是 w2.inner,即 w2 函数中 innner 函数的引用 1. 执行此函数(执行函数可以看成是直接把函数中的语句复制过来替换掉原来执行函数的语句)则打 印 ---2222222222---- 2. 然后执行传入的参数,即传入的函数;由于传入的参数是未修饰的 f1 1. 即执行 f1,打印出 ---f1--- 2. 返回上一级函数,即 w2.inner 3. 继续执行 w2.innner 函数语句,则打印:---2222222222---- 4. 执行完毕,返回上一级函数,即 w1.inner 3. 继续执行 w1.inner函数,即修饰之后的 f1 函数,打印: ---1111111111---- 4. 函数执行完毕,返回主程序
实际上,装饰其实也即是将装饰函数与原函数进行重新构造,形成新的函数的过程,是在导入模块的时候就执行了的。构造完成的函数,及其变量,只有在执行到的时候才会寻址调用。
装饰有参数的函数
上面的装饰器很简单,只适用于没有任何参数的函数。 如果有函数要接受如下的参数怎么办?
# import functools
def w1(func):
print("---正在装饰1----")
# @functools.wraps(func)
# def inner(*args, **kwargs) # 可以接受任何参数
def inner(x, y):
print("---1111111111----")
# sum = func(*args, **kwargs)
sum = func(x, y)
print("---1111111111----")
return sum
return inner
@w1
def f1(a, b):
print("---f1---")
return a+b
if __name__ == "__main__":
print("f1: ", f1)
print("w1: ", w1)
print("执行 f1()")
print(f1(2, 3))
# 结果
---正在装饰1----
f1: <function w1.<locals>.inner at 0x7f83a3417bf8>
w1: <function w1 at 0x7f83a34fe1e0>
执行 f1()
---1111111111----
---f1---
---1111111111----
5
以这种方式就可以装饰函数的参数了。
应该会注意到,装饰器中嵌套的inner()
函数的参数与其装饰的函数的参数是一样的。 考虑到这一点,现在可以让一般装饰器使用任何数量的参数。
那么,如果被装饰的函数有返回值,同样,在 inner()
里面把函数返回的东西用个变量保存起来,然后 在 inner()
里面 return
即可。
在Python中,这个由function(* args,** kwargs)
完成。 这样,args
将是位置参数的元组,kwargs
将是关键字参数的字典。
若我们在 inner() 中返回传入的函数的值,会发生什么呢?
# import functools
def w1(func):
print("---正在装饰1----")
# @functools.wraps(func)
def inner(x, y):
print("---1111111111----")
print("w1.innner")
print("---1111111111----")
return func(x,y)
return inner
@w1
def f1(a, b):
print("---f1---")
return a+b
if __name__ == "__main__":
print("f1: ", f1)
print("w1: ", w1)
print("执行 f1()")
print(f1(2, 3))
# 结果
---正在装饰1----
f1: <function w1.<locals>.inner at 0x7f783cb0cbf8>
w1: <function w1 at 0x7f783cbf31e0>
执行 f1()
---1111111111----
w1.innner
---1111111111----
---f1---
5
我们可以看到,内嵌函数返回未被装饰的函数,也就是原函数,装饰后的函数仍然是 w1.inner
函数的引用,并没有改变修饰之后的函数引用。
装饰器带参数(装饰器加括号)
若我们给装饰器之后加括号,或者再传入参数,然后再改一改,会发生什么呢?
# 模块 test.py
def w1():
print("---正在装饰1----")
def inner(func):
print("---1111111111----")
print("w1.inner")
print("---1111111111----")
return inner
@w1()
def f1():
print("---f1---")
if __name__ == "__main__":
print("f1: ", f1)
print("w1: ", w1)
print("执行 f1()")
print(f1())
# 导入模块 import test.py
---正在装饰1----
---1111111111----
w1.inner
---1111111111----
# 结果
---正在装饰1----
---1111111111----
w1.inner
---1111111111----
f1: None
w1: <function w1 at 0x7f16c606a1e0>
执行 f1()
Traceback (most recent call last):
File "test.py", line 17, in <module>
print(f1())
TypeError: 'NoneType' object is not callable
可以看出,模块导入的时候竟然连里面的 inner
函数也被执行了一遍,因为输出了 ---111111111111-----
,这说明,如果 @w1()
这样用 ,那么它首先会把 w1()
函数执行一遍 , 这个时候返回的是 inner
函数的引用,那么 @w1()
就变成了 @inner
这个时候再把 f1
传到了inner
函数里面开始进行装饰(即执行 inner
函数),所以 inner
函数被执行,由于 inner
函数的返回值是 None
,则 f1
最终的指向是 None
。
注: 区分清楚函数的引用与函数的调用。函数名是函数的地址,即函数的引用,是变量;函数名后面加括号是函数的调用,即执行函数。装饰器的 @
后面跟的是函数的引用。
利用这个特点,可以在装饰器中带有参数 ,只不过为了防止调用,需要在外面再加上一层:
import functools
def a(flag=False):
print("---正在装饰 a----")
if flag:
def w1(func):
print("---正在装饰1----")
@functools.wraps(func)
def inner():
print("---1111111111----")
print("flag: ", flag)
func()
print("---1111111111----")
return inner
return w1
else:
def w2(func):
print("---正在装饰2----")
def inner():
print("---2222222222----")
print("flag: ", flag)
func()
print("---2222222222----")
return inner
return w2
添加下面装饰器,然后执行脚本的结果为:
@a(False) # 必须要传入参数
def f1():
print("---f1---")
if __name__ == "__main__":
print("f1: ", f1)
print("执行 f1()")
print(f1())
# 导入模块 python -c 'import he'
---正在装饰 a----
---正在装饰2----
# 执行结果
---正在装饰 a----
---正在装饰2----
f1: <function a.<locals>.w2.<locals>.inner at 0x7f1f3a4ff730>
执行 f1()
---2222222222----
flag: False
---f1---
---2222222222----
None
可以看到装饰后 f1
是 a.w2.inner
的引用。过程如下:
- 先执行
a1(False)
,a1
里面用flag
这个变量保存传递的参数,返回的是w2
的引用 - 装饰器那一行 变成了
@w2
,然后把f1
传递进去,调用w2
开始进行装饰 - 装饰完成后 返回的 是
inner
的引用 所以 现在f1 = inner
我们将装饰器的参数换一下使用:
@a(True)
def f1():
print("---f1---")
if __name__ == "__main__":
print("f1: ", f1)
print("执行 f1()")
print(f1())
# 导入模块
---正在装饰 a----
---正在装饰1----
# 执行脚本
---正在装饰 a----
---正在装饰1----
f1: <function f1 at 0x7f6f258986a8>
执行 f1()
---1111111111----
flag: True
---f1---
---1111111111----
None
正好验证了我们之前的道理。@a()
会先执行函数的调用,即a()
,然后才会执行装饰器,即@
进行装饰函数。