首先预祝抗击在武汉肺炎第一线的兄弟姐妹们平安归来,撑住!你们辛苦啦!没事儿有我们共渡难关!
最近装饰器用的比较多,由此想到了闭包,再往上就是作用域的一些陷阱问题;索性记录一下,类似于一个散文形式的,只是对自己的一些想法和网上的一些资料进行梳理,想法 -> 验证 -> 查实 仅此而已,工作时候零零散散,现在拾掇一下,作为总结,希望对你有所帮助~ 新年快乐!
a = 3
def foo1(x):
b = 4
print (x + a)
print ("local", locals()) # local表示局部作用域
foo1(3)
6
local {'b': 4, 'x': 3}
a = 3
def foo2(x):
a = 4
b = 4
print (x + a)
print ("local",locals())
print(a)
print('----')
foo2(3)
3
----
7
local {'b': 4, 'a': 4, 'x': 3}
a = 3
def foo3(x):
global a
a = a + 3
print (a)
print ("local",locals())
print(a)
print('----')
foo3(3)
3
----
6
local {'x': 3}
看看下面一个例子,foo4(3)
在做些什么
def foo4(x):
def _help():
print ("local",locals())
print (x + 3)
return _help
foo4(3)
._help>
foo4(3)
执行完成后,返回的是_help
,那么如果再次对这个返回的值进行操作foo4(3)()
,实质上是构成_help()
,不就是执行一次正常的函数么?
# 验证假设
foo4(3)()
local {'x': 3}
6
以上就是我们说的最近简单的闭包 - 对内层函数和其内部引用的上层局部命名空间变量的一种封装。先说下优缺点,不明白可以先略过,稍微留点印象即可
上述的几个例子发现,在执行完foo4(3)
时,已经生成了一个_help
的函数返回,且里面还夹带了“私货”,一个外部变量3
(就是后文会讲到的自由变量),所以闭包能够自身传递内层函数所需要(引用)的变量,自身还能再传入新的变量,详细看下面一个例子
def foo5(x):
def _help(*args):
print ("local",locals())
return (x*args[0] + 3)
return _help
h = foo5(2)
print(h) # 类似于scala中的柯里化
print(h(3))
._help at 0x10441c378>
local {'args': (3,), 'x': 2}
9
foo(5)
完成了两种参数的传递,一个是构造h=foo5(2)
时传入的2
,还有一个就是后续执行h(3)
时候传递的变量3
(ps:*agrs
表示以元组的形式封装变量之后再拆包传递,不再赘述详见:*args和**kwargs
)
下面详细拆一下作用域到底体现在哪里,以及对闭包的影响
在Python程序中声明、改变、查找变量名时,都是在一个保存变量名的命名空间中进行中,此命名空间亦称为变量的作用域。python的作用域是静态的,在代码中变量名被赋值的位置决定了该变量能被访问的范围。即Python变量的作用域由变量所在源代码中的位置决定.(From: python3的local, global, nonlocal简析)
简单来说,python引用变量的顺序: 当前作用域局部变量->外层作用域变量->当前模块中的全局变量->python内置变量,如果找不到则会报错NameError: name 'x' is not defined
;其中built-in
可以类似认为函数内置方法如len
;(当然你在包内自己写len
函数,那么可以根据LEGB
原则覆盖系统自带的len
) 下面给几个例子:
def foo6(x):
a = 5 # 不再被使用
def _help(*args):
a = 0 # 在闭包内找到a, 最先找到,直接使用; Local 可能是在一个函数或者类方法内部。
print ("local",locals())
return (x*args[0] + a)
return _help
h = foo6(2)
print(h)
print(h(3))
._help at 0x10441cae8>
local {'a': 0, 'args': (3,), 'x': 2}
6
def foo7(x):
a = 5 # Enclosed 可能是嵌套函数内,比如说 一个函数包裹在另一个函数内部。
def _help(*args):
print ("local",locals())
return (x*args[0] + a)
return _help
h = foo7(2)
print(h)
print(h(3))
# 相比较foo6,a在_help内部没有,只有往上层找
._help at 0x104172f28>
local {'args': (3,), 'x': 2, 'a': 5}
11
a = 0
def foo8(x):
def _help(*args):
print ("local",locals())
return (x*args[0] + a)
return _help
h = foo8(3)
print(h)
print(h(3))
._help at 0x1045da268>
local {'args': (3,), 'x': 3}
9
local variable 'xx' referenced before assignment
的体现一些“显而易见”的使用方法可能并不是按照我们预期的那样发展,这就是作用域陷阱,有时候也会被认为是闭包陷阱,实质上都是作用域的选择导致的问题
def foo9(x):
a = 0
def _help(*args):
a += 1
print ("local",locals())
return (x*args[0] + a)
return _help
h = foo9(3)
print("局部变量:",h.__code__.co_varnames) # 局部变量
print("自由变量:",h.__code__.co_freevars) # 自由变量
print(h(3))
局部变量: ('args', 'a')
自由变量: ('x',)
UnboundLocalError Traceback (most recent call last)
in ()
14 print("局部变量:",h.__code__.co_varnames) # 局部变量
15 print("自由变量:",h.__code__.co_freevars) # 自由变量
---> 16 print(h(3))
in _help(*args)
6 a = 0
7 def _help(*args):
----> 8 a += 1
9 print ("local",locals())
10 return (x*args[0] + a)
UnboundLocalError: local variable 'a' referenced before assignment
对于数字,字符串,元组等不可变的类型来说,a +=1
只能读取不能更新,如果尝试重新绑定,相当于 a = a + 1
操作,会隐式创建局部变量 a
,这样执行h = foo9(3)
的时候,启动去定义函数_help
,会先判断是否有需要引用到上游的变量,如果自己这一层有这个变量a
,那么上层的a
就不是自由变量了,会被销毁,没办法保存在闭包中;再举个简单的例子如下所示
b = 3
def bar(x):
print (x)
print (b)
b = 9
print("局部变量:",bar.__code__.co_varnames) # 局部变量
print("自由变量:",bar.__code__.co_freevars) # 自由变量
bar = bar(3)
# 编译函数体时,发现b被赋值(b=9),所以不采用向上找变量 b = 3,当真要启动的时候,发现要用b,却找不到值
局部变量: ('x', 'b')
自由变量: ()
3
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
in ()
8 print("局部变量:",bar.__code__.co_varnames) # 局部变量
9 print("自由变量:",bar.__code__.co_freevars) # 自由变量
---> 10 bar = bar(3)
11
12 # 编译函数体时,发现b被赋值(b=9),所以不采用向上找变量 b = 3,当真要启动的时候,发现要用b,却找不到值
in bar(x)
2 def bar(x):
3 print (x)
----> 4 print (b)
5 b = 9
6
UnboundLocalError: local variable 'b' referenced before assignment
其实从字节码的角度去考虑会更加清楚一些;我们知道python通过cpython解释器解释成机器能够执行的字节码去操作的;如果凑巧你还了解dis
这个神奇的包,那就更好啦
b = 3
def bar(x):
print (x)
print (b)
b = 9
import dis
dis.dis(bar)
3 0 LOAD_GLOBAL 0 (print)
2 LOAD_FAST 0 (x) 加载局部变量
4 CALL_FUNCTION 1
6 POP_TOP
4 8 LOAD_GLOBAL 0 (print)
10 LOAD_FAST 1 (b) 加载局部变量
12 CALL_FUNCTION 1
14 POP_TOP
5 16 LOAD_CONST 1 (9) 加载常量
18 STORE_FAST 1 (b) 局部变量赋值
20 LOAD_CONST 0 (None)
22 RETURN_VALUE
那么去掉里面的 b=9
会如何呢?
b = 3
def bar(x):
print (x)
print (b)
import dis
dis.dis(bar)
3 0 LOAD_GLOBAL 0 (print)
2 LOAD_FAST 0 (x) 加载局部变量
4 CALL_FUNCTION 1
6 POP_TOP
4 8 LOAD_GLOBAL 0 (print)
10 LOAD_GLOBAL 1 (b) 加载全局变量
12 CALL_FUNCTION 1
14 POP_TOP
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
从上面两个例子可以看出,其实print
操作的时候,需要对b
进行引用,而函数内部有b
的局部变量被申明(加载),只是还未被赋值(参考第一个例子中对b
的STORE_FAST
操作),所以导致不再往上查找变量,所以全局变量b=3
根本没有引用,也就导致报错的问题了;详细的字节码相关的可以参考:python 字节码死磕,dis包官方文档可以参考:dis — Disassembler for Python bytecode,这里不做赘述;
再来个例子 说明 foo9的情况
def foo9_2(x):
a = 0
def _help(*args):
c = a + 1
a = c
return (x*args[0] + a)
return _help
h = foo9_2(3)
print("局部变量:",h.__code__.co_varnames) # 局部变量
print("自由变量:",h.__code__.co_freevars) # 自由变量
print(h(3))
局部变量: ('args', 'a', 'c')
自由变量: ('x',)
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
in ()
12 print("局部变量:",h.__code__.co_varnames) # 局部变量
13 print("自由变量:",h.__code__.co_freevars) # 自由变量
---> 14 print(h(3))
in _help(*args)
4 a = 0
5 def _help(*args):
----> 6 c = a + 1
7 a = c
8 return (x*args[0] + a)
UnboundLocalError: local variable 'a' referenced before assignment
再来一个体会一下
def foo9_3(x):
a = 0
def _help(*args):
c = a + 1
# a = c
return (x*args[0] + a)
return _help
h = foo9_3(3)
print("局部变量:",h.__code__.co_varnames) # 局部变量
print("自由变量:",h.__code__.co_freevars) # 自由变量
print(h(3))
局部变量: ('args', 'c')
自由变量: ('a', 'x')
9
通过比较foo9_3
和foo9_2
可知,当执行完h=foo9_2(3)
时,发现在_help
中已经申明了局部变量a
,因为python
在编译函数体时,它判断a
是局部变量(在函数体内被赋值:a = c
),所以外层函数的自由变量a
认为自己将不被引用,将被销毁;而调用_help
时,会从局部变量开始往上找a
,而局部变量a
压根就没绑定值;而在foo9_3
中,c = a + 1
申明为内层函数的局部变量,而且在内也没有再次申明局部变量a
,所以根据LEGB
原则,往上层找,找到上层的局部变量a = 0
,被引用。既然知道了作用域陷阱带来的问题,那么其实可以比较好的解决上述的一些问题达到期望达到的诉求
def foo9_1(x):
a = 0
def _help(*args,z=a):
z += 1
print ("local",locals()) # 对于local来说,观察的是针对这个函数生效的变量,所以上游的自由变量和这个函数内的局部变量都被计算
return (x*args[0] + z)
return _help
h = foo9_1(3)
print("局部变量:",h.__code__.co_varnames) # 局部变量
print("自由变量:",h.__code__.co_freevars) # 自由变量
print(h(3))
局部变量: ('z', 'args')
自由变量: ('x',)
local {'args': (3,), 'z': 1, 'x': 3}
10
一个比较重要的点是,函数是在定义时候传递进入的参数生效,并不需要执行的时候才生效,h = foo9_1(3)
时,其中的变量a已经随着构造h
的时候赋值给了z
,所以h = foo9_1(3)
执行完后a
被销毁,因为下游不要引用到,取而代之的是z
,所以当z+=1
时,直接进行+1
操作;下面是描述函数在定义时参数生效的例子(from:stackoverflow上高赞的回答)
# python 默认参数是在函数定义的时候已经求值,而不是调用的时候
import time
def report(when=time.strftime("%Y-%m-%d %X",time.localtime())):
print (when)
report()
time.sleep(1)
report()
print('--------')
def report2():
print(time.strftime("%Y-%m-%d %X",time.localtime()))
report2()
time.sleep(1)
report2()
2020-01-25 14:39:00
2020-01-25 14:39:00
--------
2020-01-25 14:39:01
2020-01-25 14:39:02
nonlocal
来申明;nonlocal
关键字用来在函数或其他作用域中使用外层(非全局)变量
def foo10(x):
a = 0
def _help(*args):
nonlocal a
a += 1
print ("local",locals())
return (x*args[0] + a)
return _help
h = foo10(3)
print("局部变量:",h.__code__.co_varnames) # 局部变量
print("自由变量:",h.__code__.co_freevars) # 自由变量
print(h(3))
局部变量: ('args',)
自由变量: ('a', 'x')
local {'args': (3,), 'x': 3, 'a': 1}
10
其实nonlocal
和global
有个异曲同工之妙,可以说是打通两层之间的桥梁,nonlocal
是打通L->E
,global
是 ?-> G
a = 1
def foo10(x):
a = 0
def _help(*args):
global a
a += 1
print ("local",locals())
return (x*args[0] + a)
return _help
h = foo10(3)
print("局部变量:",h.__code__.co_varnames) # 局部变量
print("自由变量:",h.__code__.co_freevars) # 自由变量
print(h(3)) # 3*3 + 2
print(a)
局部变量: ('args',)
自由变量: ('x',)
local {'args': (3,), 'x': 3}
11
2
可以发现,global
在函数内部改变全局变量,而nonlocal
则是改变嵌套函数内空间的变量也就是Enclosed
def foo11(x):
a = []
def _help(*args):
a.append(0)
print ("local",locals())
return (x*args[0] + a[0])
return _help
h = foo11(3)
print("局部变量:",h.__code__.co_varnames) # 局部变量
print("自由变量:",h.__code__.co_freevars) # 自由变量
print(h(3))
局部变量: ('args',)
自由变量: ('a', 'x')
local {'args': (3,), 'x': 3, 'a': [0]}
9
在foo9
的例子中,对于不可变变量,只能被读取不能被更新,我们所谓的更新只是变量的重新绑定;而对于列表来说,内部是可变的,可以理解为一个不可变的容器但是容器内部可变,可以看下面的例子理解下
x = 3
print(id(x))
x = 4
print(id(x))
q = []
print(id(q))
q.append(3)
print(id(q))
4332976768
4332976800
4368241864
4368241864
特别是在循环体中变量的产生和销毁过程中,形成陷阱
def foo12(x):
fs = []
for _ in range(x):
def _help(*args):
print ("local",locals())
return ("id(_)={id_}; x={x}; _*args[0]={res}".format(id_=id(_) ,x= x, res = _*args[0]))
fs.append(_help)
return fs
h = foo12(4)
s = [_(3) for _ in h]
print (s)
local {'args': (3,), 'x': 4, '_': 3}
local {'args': (3,), 'x': 4, '_': 3}
local {'args': (3,), 'x': 4, '_': 3}
local {'args': (3,), 'x': 4, '_': 3}
['id(_)=4332976768; x=4; _*args[0]=9', 'id(_)=4332976768; x=4; _*args[0]=9', 'id(_)=4332976768; x=4; _*args[0]=9', 'id(_)=4332976768; x=4; _*args[0]=9']
id(_)
并没有按照想象的那样,每次记录下循环的变量值,而是直接采用的是循环的末尾值,使用__closure__
属性和cell
对象来观察闭包中的自由变量(并未在本地作用域中绑定的变量)
__closure__
属性,如果这个函数是一个闭包的话,那么它返回的是一个由cell
对象 组成的元组对象。cell
对象的cell_contents
属性就是闭包中的自由变量。for index_,value in enumerate(h):
print (value, [i.cell_contents for i in value.__closure__])
._help at 0x10441c158> [3, 4]
._help at 0x10441cf28> [3, 4]
._help at 0x10441cd08> [3, 4]
._help at 0x104172f28> [3, 4]
可以发现闭包内包含的外部变量有传入的遍历时候的变量 _
还有 x
,都是属于传入闭包的外部变量
def foo13(x):
fs = []
for _ in range(x):
def _help(*args,m=_):
print ("local",locals())
return ("id(m)={idm}; _={_}; x={x}; m*args[0]={res}".format(idm=id(m), _=_ ,x= x, res = m*args[0]))
fs.append(_help)
return fs
h = foo13(4)
s = [_(3) for _ in h]
print (s)
for index_,value in enumerate(h):
print (value, [i.cell_contents for i in value.__closure__])
local {'args': (3,), 'm': 0, 'x': 4, '_': 3}
local {'args': (3,), 'm': 1, 'x': 4, '_': 3}
local {'args': (3,), 'm': 2, 'x': 4, '_': 3}
local {'args': (3,), 'm': 3, 'x': 4, '_': 3}
['id(m)=4332976672; _=3; x=4; m*args[0]=0', 'id(m)=4332976704; _=3; x=4; m*args[0]=3', 'id(m)=4332976736; _=3; x=4; m*args[0]=6', 'id(m)=4332976768; _=3; x=4; m*args[0]=9']
._help at 0x1045ea730> [3, 4]
._help at 0x1045ea620> [3, 4]
._help at 0x1045ead08> [3, 4]
._help at 0x1045eab70> [3, 4]
可以发现闭包内包含的外部变量只有传入的 x
,还有最后被引用的循环边界 _
,而没有被赋值的m
所以解释了为什么foo13
中没有外部变量_
, 因为在定义函数_help
的时候(并不需要被执行),已经将 _
传递给了m
进行保存了,fs
中保存了四个封装好的_help
函数,s = [_(3) for _ in h]
这一步相当于给这几个函数都传递了3
这个参数进去;但当执行这一步之前,已经完成h = foo13(4)
,表示已经完成了执行,循环结束,_
已经到了 3
,(循环体内的值不断进行销毁,保留到最后被引用)所以当调用_help
时候,_help
就会根据LEGB
的方式一级级往上找需要用到的_
变量,_
这个时候就是遍历完后最后的值;可以参考下:程序员必知的Python陷阱与缺陷列表
举两个例子来描述一下这个现象,特别是针对参数中传入可变参数时需要注意的一些情况;在《编写高质量Python代码的59个有效方法》中提到过,参数的初始化传递最好使用赋值None
的方式处理,这样对需要用到的时候可被进行初始化,避免一些变量泄露的情况出现“反常识”的现象
def report_1(data, list_=[]):
print ("local",locals())
list_.append(data)
return list_
f1 = report_1("test1")
f1.append("test1_1")
print(f1)
print(id(f1))
print('------------------')
f2 = report_1("test2")
f2.append("test2_1")
print(f2) # 在f1的情况下继续append
print(id(f2))
print('------------------')
f3 = report_1("test3",list_=[])
f3.append("test3_1")
print(f3)
print(id(f3))
local {'list_': [], 'data': 'test1'}
['test1', 'test1_1']
4367345800
------------------
local {'list_': ['test1', 'test1_1'], 'data': 'test2'}
['test1', 'test1_1', 'test2', 'test2_1']
4367345800
------------------
local {'list_': [], 'data': 'test3'}
['test3', 'test3_1']
4368427400
在执行f1 = report_1("test1")
的时候,参数为一个空列表list_=[]
,相当于全局变量,当函数内部不再指定新同名变量时,用的就是全局变量;有一个很好的执行可视化的网站:http://www.pythontutor.com/visualize.html#mode=display 推荐给大家,可以一步步看到执行之后变量的存放等等
# 解决方法也比较简单,先将默认参数置None,要用的时候再使用
def report_2(data, list_=None):
if list_ is None:
list_ = []
print ("local",locals())
list_.append(data)
return list_
f1 = report_2("test1")
f1.append("test1_1")
print(f1)
print(id(f1))
print('------------------')
f2 = report_2("test2")
f2.append("test2_1")
print(f2)
print(id(f2))
local {'list_': [], 'data': 'test1'}
['test1', 'test1_1']
4367815624
------------------
local {'list_': [], 'data': 'test2'}
['test2', 'test2_1']
4368472712
由此深入的还有两个例子
def foo13_1(x):
fs = []
for _ in range(x):
def _help(*args,m=[]):
m.append(_)
return (id(m), x, m[-1]*args[0])
fs.append(_help)
return fs
h = foo13_1(4)
s = [_(3) for _ in h]
print (s)
for index_,value in enumerate(h):
print (value, [i.cell_contents for i in value.__closure__])
[(4489292616, 4, 9), (4490954888, 4, 9), (4491173640, 4, 9), (4491173704, 4, 9)]
._help at 0x10b9f48c8> [3, 4]
._help at 0x10b9f4a60> [3, 4]
._help at 0x10b9f4378> [3, 4]
._help at 0x10b9f4e18> [3, 4]
def foo13_2(x):
fs = []
for _ in range(x):
def _help(*args,m=None,_=_):
m = []
m.append(_)
return (id(m), x, m[-1]*args[0])
fs.append(_help)
return fs
h = foo13_2(4)
s = [_(3) for _ in h]
print (s)
for index_,value in enumerate(h):
print (value, [i.cell_contents for i in value.__closure__])
[(4491347912, 4, 0), (4491347912, 4, 3), (4491347912, 4, 6), (4491347912, 4, 9)]
._help at 0x10bb1e840> [4]
._help at 0x10bb1e9d8> [4]
._help at 0x10bb1e378> [4]
._help at 0x10bb1e2f0> [4]
了解完闭包的一些性质后,再来看装饰器(还不清楚的可以参考:理解Python装饰器(Decorator),其实就是带有语法糖@
的闭包高阶的用法.实质上,装饰器感觉更多的是对闭包更多的是一种对闭包思想的一种优雅表达,特别是一些语法糖的使用会显得尤其优雅简洁;简单说,装饰器可以在不改变原始函数的情况下,给函数添加一些额外的功能;看过一篇知乎的回答比较形象,普通函数就像一个人穿了一件T恤,然后闭包就是各种外套啊,帽子什么的,根据需求进行增加,这样不会破坏被装饰的函数;下面举两个例子
需求:对一个函数采用重试机制,每过一段时间重试一次
import time
def pri():
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "execute time: %s"%(time_)
print (p)
pri()
execute time: 2020-01-25 19:37:48
# 需要牢记的一点事,在python中,一切都是对象,当然包括函数,可以将函数当做参数传递进去
def retry(fn):
def wrap():
for _ in range(1,4):
print ("check %s times" % _)
fn()
time.sleep(1)
return wrap
def pri():
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "execute time: %s"%(time_)
print (p)
rep = retry(pri) # 此时rep返回的是一个wrap函数,当执行wrap()后,内部函数开始执行
print(rep)
rep()
.wrap at 0x1045da048>
check 1 times
execute time: 2020-01-25 19:37:44
check 2 times
execute time: 2020-01-25 19:37:45
check 3 times
execute time: 2020-01-25 19:37:46
这个可以看foo4
和foo5
两个例子,这里不做赘述;利用@语法糖,将上述代码再一次精简,实现的效果是等效的,包括内部执行也是一样
def retry(fn):
def wrap():
for _ in range(1,4):
print ("check %s times" % _)
fn()
time.sleep(1)
return wrap
@retry
def pri():
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "execute time: %s"%(time_)
print (p)
pri()
check 1 times
execute time: 2020-01-25 19:39:22
check 2 times
execute time: 2020-01-25 19:39:23
check 3 times
execute time: 2020-01-25 19:39:24
上述的例子其中的重试次数在代码中写死了,这个对于需求来说只能说够用但是不够灵活,如果针对不同的函数需要不同的延迟时间和不同的重试次数,这个需要去修改代码了,这个很不优雅的行为,好在装饰器可以传递参数
# 装饰器中传递参数
def retry(x,y):
x = x*y
# ----- 这里下面和上面完全一样,不一样的是再次使用闭包的性质往上套了一层,用来接收参数 x,y
def myretry(fn):
def wrap():
for _ in range(1,x):
print ("check %s times" % _)
fn()
time.sleep(1)
return wrap
# -----
return myretry
def pri():
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "execute time: %s"%(time_)
print (p)
# 方式1
# f = retry(2,3)
# rep = f(pri)
# 方式2
rep = retry(2,3)(pri)
rep()
print ([i.cell_contents for i in rep.__closure__])
# [, 6] 可见只是保存了x的值为自由变量
check 1 times
execute time: 2020-01-25 19:41:24
check 2 times
execute time: 2020-01-25 19:41:25
check 3 times
execute time: 2020-01-25 19:41:26
check 4 times
execute time: 2020-01-25 19:41:27
check 5 times
execute time: 2020-01-25 19:41:28
[, 6]
同样改造成使用语法糖@的方式,这样代码更精简
# 使用语法糖@的方式
def retry(x,y):
x = x*y
# --------
def myretry(fn):
def wrap():
for _ in range(1,x):
print ("check %s times" % _)
fn()
time.sleep(1)
return wrap
# --------
return myretry
@retry(2,3) # 区别
def pri():
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "execute time: %s"%(time_)
print (p)
pri()
check 1 times
execute time: 2020-01-25 19:41:59
check 2 times
execute time: 2020-01-25 19:42:00
check 3 times
execute time: 2020-01-25 19:42:01
check 4 times
execute time: 2020-01-25 19:42:02
check 5 times
execute time: 2020-01-25 19:42:03
def retry(x,y):
x = x*y
# --------
def myretry(fn):
def wrap(*args,**kwargs): # 这里*args,**kwargs用来监控被包装函数的传递参数
for _ in range(1,x):
print ("check %s times" % _)
fn(*args, param=(x, _), **kwargs) # 甚至可以构造包装函数中的自由变量进入传递
time.sleep(1)
return wrap
# --------
return myretry
@retry(2,3)
def pri(x, param=None):
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
print (p)
pri(3)
check 1 times
owner param:3; execute time: 2020-01-25 19:42:08; receive param:(6, 1)
check 2 times
owner param:3; execute time: 2020-01-25 19:42:09; receive param:(6, 2)
check 3 times
owner param:3; execute time: 2020-01-25 19:42:10; receive param:(6, 3)
check 4 times
owner param:3; execute time: 2020-01-25 19:42:11; receive param:(6, 4)
check 5 times
owner param:3; execute time: 2020-01-25 19:42:12; receive param:(6, 5)
既然我可以给函数“套上毛衣”,那么我再这个毛衣外面再套“一层外套”不也可以么,的确,装饰器支持多层嵌套
# 多层装饰
def log(fn):
'''
执行时候打一遍日志
'''
def wrap(*args,**kwargs):
print ("log begin... wrapped function name: %s"%(fn.__name__))
fn(*args,**kwargs)
return wrap
def retry(x,y):
x = x*y
def myretry(fn):
def wrap(*args,**kwargs):
for _ in range(1,x):
print ("check %s times" % _)
fn(*args, param=(x, _), **kwargs)
time.sleep(1)
return wrap
return myretry
@log
@retry(2,3)
def pri(x, param=None):
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
print (p)
pri(3)
log begin... wrapped function name: wrap
check 1 times
owner param:3; execute time: 2020-01-25 19:44:27; receive param:(6, 1)
check 2 times
owner param:3; execute time: 2020-01-25 19:44:28; receive param:(6, 2)
check 3 times
owner param:3; execute time: 2020-01-25 19:44:29; receive param:(6, 3)
check 4 times
owner param:3; execute time: 2020-01-25 19:44:30; receive param:(6, 4)
check 5 times
owner param:3; execute time: 2020-01-25 19:44:31; receive param:(6, 5)
关于多层装饰器执行顺序是由底部开始往上嵌套,最接近函数的包装最先执行
# 关于多层装饰器执行顺序的问题
def log(fn):
def wrap(*args,**kwargs):
print ("log begin... wrapped function name: %s"%(fn.__name__))
fn(*args,**kwargs)
return wrap
def retry(x,y):
x = x*y
def myretry(fn):
def wrap(*args,**kwargs):
for _ in range(1,x):
print ("check %s times" % _)
fn(*args, param=(x, _), **kwargs)
time.sleep(1)
return wrap
return myretry
# @log
# @retry(2,3)
def pri(x, param=None):
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
print (p)
# 执行顺序从底向上一层层嵌套
f = retry(2,3)(pri)
g = log(f)
g(3)
log begin... wrapped function name: wrap
check 1 times
owner param:3; execute time: 2020-01-25 19:45:32; receive param:(6, 1)
check 2 times
owner param:3; execute time: 2020-01-25 19:45:33; receive param:(6, 2)
check 3 times
owner param:3; execute time: 2020-01-25 19:45:34; receive param:(6, 3)
check 4 times
owner param:3; execute time: 2020-01-25 19:45:35; receive param:(6, 4)
check 5 times
owner param:3; execute time: 2020-01-25 19:45:36; receive param:(6, 5)
装饰器参数有时候可有可无,如果限定死必须传递参数又会给代码显得冗余,毕竟谁也不想出现
@log()
来替换@log
这样优雅的方式把;在此之前先说明一下偏函数用法,至于详细的使用自行百度
from functools import partial
def partial_(x, y=None, z=None):
return x+y+z
p = partial(partial_, 2, y=1)
print(p)
p(z=3)
functools.partial(, 2, y=1)
6
利用偏函数保存变量,部分执行的特性,可以用于构造接受/不接受参数的装饰器
# 使用偏函数设置可选传入参数
from functools import wraps, partial
def myretry(fn=None, *, x=None, y=None):
if fn is None:
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
# 装饰器的其中一个特性就是只会在函数定义的时候应用一次,所以这个时刻是函数定义的时候并非执行的时刻
print ("load myretry time:%s" % time_)
return partial(myretry ,x=x, y=y)
x = x if x else 3
y = y if y else 2
@wraps(fn)
def wrap(*args,**kwargs):
for _ in range(1,x):
print ("check %s times" % _)
r = fn(*args, param=(x, _), **kwargs)
time.sleep(1)
print (r)
return r
return wrap
def pri(x, param=None):
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
return p
def pri_1(x, param=None):
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
return p
pri_partial = myretry(x=4,y=3) # 因为没有传递fn,则默认传递fn=None,会返回一个限定部分参数(x,y)的偏函数
print(pri_partial)
rep_pri = pri_partial(pri)
print ([i.cell_contents for i in rep_pri.__closure__]) # 这里只有一个 x的自由变量,_是即产即销,直接调用运行的,
rep_pri(3)
print("--------------")
rep_pri_1 = myretry(pri) # 因为传递fn,则x,y都被设置成默认3,2
print ([i.cell_contents for i in rep_pri_1.__closure__])
rep_pri_1(3)
load myretry time:2020-01-20 11:14:09
functools.partial(, x=4, y=3)
[, 4]
check 1 times
owner param:3; execute time: 2020-01-20 11:14:09; receive param:(4, 1)
check 2 times
owner param:3; execute time: 2020-01-20 11:14:10; receive param:(4, 2)
check 3 times
owner param:3; execute time: 2020-01-20 11:14:11; receive param:(4, 3)
--------------
[, 3]
check 1 times
owner param:3; execute time: 2020-01-20 11:14:12; receive param:(3, 1)
check 2 times
owner param:3; execute time: 2020-01-20 11:14:13; receive param:(3, 2)
'owner param:3; execute time: 2020-01-20 11:14:13; receive param:(3, 2)'
from functools import wraps, partial
def myretry(fn=None, *, x=None, y=None):
if fn is None:
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
# 装饰器的其中一个特性就是只会在函数定义的时候应用一次,所以这个时刻是函数定义的时候并非执行的时刻
print ("load myretry time:%s" % time_)
return partial(myretry ,x=x, y=y)
x = x if x else 3
y = y if y else 2
@wraps(fn)
def wrap(*args,**kwargs):
for _ in range(1,x):
print ("check %s times" % _)
r = fn(*args, param=(x, _), **kwargs)
time.sleep(1)
print (r)
return r
return wrap
@myretry(x=4,y=3)
def pri(x, param=None):
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
return p
@myretry
def pri_1(x, param=None):
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
return p
print("-----启动执行-----")
time.sleep(2)
pri(3)
print("-----------")
pri_1(3)
load myretry time:2020-01-20 11:21:55
-----启动执行-----
check 1 times
owner param:3; execute time: 2020-01-20 11:21:57; receive param:(4, 1)
check 2 times
owner param:3; execute time: 2020-01-20 11:21:58; receive param:(4, 2)
check 3 times
owner param:3; execute time: 2020-01-20 11:21:59; receive param:(4, 3)
-----------
check 1 times
owner param:3; execute time: 2020-01-20 11:22:00; receive param:(3, 1)
check 2 times
owner param:3; execute time: 2020-01-20 11:22:01; receive param:(3, 2)
'owner param:3; execute time: 2020-01-20 11:22:01; receive param:(3, 2)'
上述的例子说明装饰器会在函数定义的时候应用一次,所以这个时刻是函数定义的时候也就是@myretry(x=4,y=3)
时,已经开始应用了,所以load myretry time:2020-01-20 11:21:55
被打印出来了,可以看做一个装饰器是一个偏函数的无参构造器情况
再回到多层装饰器这个例子,我们发现出现了
log begin... wrapped function name: wrap
可以看出,由于装饰器的使用,被包装的函数损失了一些重要的元数据,比如函数名,函数注解及调用签名等都会丢失,如上述的例子中fn.__name__
返回的是被包装函数的替身wrap
;可以使用functools中的wraps来装饰底层的包装函数,这样就可以实现保存函数的元数据
# 使用装饰器保存函数元数据
from functools import wraps
def log(fn):
@wraps(fn)
def wrap(*args,**kwargs):
print ("log begin... wrapped function name: %s"%(fn.__name__))
fn(*args,**kwargs)
return wrap
def retry(x,y):
x = x*y
def myretry(fn):
@wraps(fn)
def wrap(*args,**kwargs):
for _ in range(1,x):
print ("check %s times" % _)
fn(*args, param=(x, _), **kwargs)
time.sleep(1)
return wrap
return myretry
@log
@retry(2,3)
def pri(x, param=None):
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
print (p)
pri(3)
log begin... wrapped function name: pri
check 1 times
owner param:3; execute time: 2020-01-25 19:46:18; receive param:(6, 1)
check 2 times
owner param:3; execute time: 2020-01-25 19:46:19; receive param:(6, 2)
check 3 times
owner param:3; execute time: 2020-01-25 19:46:20; receive param:(6, 3)
check 4 times
owner param:3; execute time: 2020-01-25 19:46:21; receive param:(6, 4)
check 5 times
owner param:3; execute time: 2020-01-25 19:46:22; receive param:(6, 5)
最后,如果说我调用函数的时候,只需要函数本身,并不需要其装饰后的函数,那怎么办呢?总不能把别人的代码去掉装饰器吧,这会影响影响到别人对这个函数的调用;最好的方法就是使用解包的方式;将函数去掉装饰性质,称为解包
from inspect import unwrap
#解包见参考 https://www.cnblogs.com/blackmatrix/p/6875359.html
def log(fn):
@wraps(fn)
def wrap(*args,**kwargs):
print ("log begin... wrapped function name: %s"%(fn.__name__))
r = fn(*args,**kwargs)
return r
return wrap
@log
def pri(x, param=None):
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
return p
unwrap_pri = unwrap(pri)
r = unwrap_pri(3)
print(r)
owner param:3; execute time: 2020-01-25 19:54:24; receive param:None
但有个前提,装饰器函数需要注入@wraps(func)
来获取原始需要解包的函数,如以下的方式是解包失效的
from inspect import unwrap
def log(fn):
def wrap(*args,**kwargs):
print ("log begin... wrapped function name: %s"%(fn.__name__))
r = fn(*args,**kwargs)
return r
return wrap
@log
def pri(x, param=None):
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
return p
unwrap_pri = unwrap(pri)
r = unwrap_pri(3)
print(r)
log begin... wrapped function name: pri
owner param:3; execute time: 2020-01-25 19:54:17; receive param:None
上述例子我们知道使用装饰器的核心是:Python中万物皆对象,即使是数字、字符串、函数、方法、类、模块,所以能够本能的将方法当做参数传递进入一个方法中进行处理,回顾下,左右两边等效,只是python使用@
语法糖的方式进行简化了
那么是否存在一个方法,让类也能够像函数一样调用呢?有的,__call__
;关于__call__
这里不做展开,详见这里
class Foo(object):
def __init__(self,age=None):
self.age = age
print("init")
def __call__(self,*args,**kwargs):
print("call param: %s; init param:%s" % (args[0], self.age))
foo = Foo(age=23)
print("-"*20)
foo(34)
init
--------------------
call param: 34; init param:23
既然可以从将一个实例进行函数一样的调用,那么再配合上装饰器的语法性质,是否可以实现类装饰器呢?答案当然是可以的
import time
class Log(object):
def __init__(self):
self.count = 0
self.clock = time.strftime("%Y-%m-%d %X",time.localtime())
print ("Log begin... init time: %s"%(self.clock))
def __call__(self, func):
'''
__call__的作用:将类的实例化作为像函数一样可以被调用和传参
'''
def wrap(*args,**kwargs):
self.count +=1
print("__call__ execu time: %s" % self.count)
self.execu_log()
return func(*args,count=self.count,**kwargs)
return wrap
def execu_log(self):
call_clock = time.strftime("%Y-%m-%d %X",time.localtime())
print("exec log: %s" % call_clock)
@Log()
def pri(x, count=None, param=None):
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "owner param:%s; execute time: %s; receive param:%s; call log class:%s times"%(x, time_, param, count)
return p
for _ in range(5):
time.sleep(1)
print(pri(_))
print("-"*50)
Log begin... init time: 2020-01-10 11:16:32
__call__ execu time: 1
exec log: 2020-01-10 11:16:33
owner param:0; execute time: 2020-01-10 11:16:33; receive param:None; call log class:1 times
--------------------------------------------------
__call__ execu time: 2
exec log: 2020-01-10 11:16:34
owner param:1; execute time: 2020-01-10 11:16:34; receive param:None; call log class:2 times
--------------------------------------------------
__call__ execu time: 3
exec log: 2020-01-10 11:16:35
owner param:2; execute time: 2020-01-10 11:16:35; receive param:None; call log class:3 times
--------------------------------------------------
__call__ execu time: 4
exec log: 2020-01-10 11:16:36
owner param:3; execute time: 2020-01-10 11:16:36; receive param:None; call log class:4 times
--------------------------------------------------
__call__ execu time: 5
exec log: 2020-01-10 11:16:37
owner param:4; execute time: 2020-01-10 11:16:37; receive param:None; call log class:5 times
--------------------------------------------------
如果要在原有的基础上进行扩展,也非常简单,使用类的继承就可以了,不仅保留原来的方法,还可以增加新的方法
import time
class Log(object):
def __init__(self):
self.count = 0
self.clock = time.strftime("%Y-%m-%d %X",time.localtime())
print ("Log begin... init time: %s"%(self.clock))
def __call__(self, func):
'''
__call__的作用:将类的实例化作为像函数一样可以被调用和传参
'''
def wrap(*args,**kwargs):
self.count +=1
print("__call__ execu time: %s" % self.count)
self.execu_log()
return func(*args,count=self.count,**kwargs)
return wrap
def execu_log(self):
call_clock = time.strftime("%Y-%m-%d %X",time.localtime())
print("exec log: %s" % call_clock)
# 使用继承的方法,很容易对已有的记录日志的类添加一些新的功能来进行扩展
class SubLog(Log):
def __init__(self,email_addr="[email protected]"):
super().__init__()
self.email_addr = email_addr
def execu_log(self):
super().execu_log()
print("send email to :%s" % self.email_addr)
@SubLog(email_addr = "[email protected]")
def pri(x, count=None, param=None):
time_ = time.strftime("%Y-%m-%d %X",time.localtime())
p = "owner param:%s; execute time: %s; receive param:%s; call log class:%s times"%(x, time_, param, count)
return p
for _ in range(5):
time.sleep(1)
print(pri(_))
print("-"*50)
Log begin... init time: 2020-01-10 11:16:10
__call__ execu time: 1
exec log: 2020-01-10 11:16:11
send email to :husky@gmail.com
owner param:0; execute time: 2020-01-10 11:16:11; receive param:None; call log class:1 times
--------------------------------------------------
__call__ execu time: 2
exec log: 2020-01-10 11:16:12
send email to :husky@gmail.com
owner param:1; execute time: 2020-01-10 11:16:12; receive param:None; call log class:2 times
--------------------------------------------------
__call__ execu time: 3
exec log: 2020-01-10 11:16:13
send email to :husky@gmail.com
owner param:2; execute time: 2020-01-10 11:16:13; receive param:None; call log class:3 times
--------------------------------------------------
__call__ execu time: 4
exec log: 2020-01-10 11:16:14
send email to :husky@gmail.com
owner param:3; execute time: 2020-01-10 11:16:14; receive param:None; call log class:4 times
--------------------------------------------------
__call__ execu time: 5
exec log: 2020-01-10 11:16:15
send email to :husky@gmail.com
owner param:4; execute time: 2020-01-10 11:16:15; receive param:None; call log class:5 times
--------------------------------------------------