python装饰器与闭包详解(看着一篇就够了)
什么是闭包?
闭包简单来说就是函数中嵌套函数。
复杂点讲其实是指延伸了作用于的函数,具备自由变量,在第一层函数中定义自由变量,在深入的函数中就可以调用该自由变量
关于闭包的核心关键点—-自由变量
什么是自由变量?
自由变量:指未在本地作用于中绑定的变量,可以将超函数中的本地作用域中的局部变量作为自由变量。
对于可变序列类型(不可散列对象):闭包中,可变序列类型的变量等价于自由变量,例如list.append()
追加方法,并不会改变引用的对象,因而会一直保持自由变量特性。
对于不可变序列类型(可散列对象):闭包中,不可变序列类型的变量必须要用nonlocal声明其为自由变量。为什么呢?看下面这个例子
1 2 3 4 5 6 7
| def counts(): sums = 0 def compute(): sums += 1 print(sum) return compute() counts()
|
报错:UnboundLocalError: local variable 'sums' referenced before assignment
原因分析:
因为在counts()
定义了不可变序列类型sums,而在compute()
中sums+=1
等价于先对sum + 1
,然后产生新的局部变量赋值给sum,会产生异常,原因是sums变成了局部变量,sum + 1
赋值给了一个未初始化的不在内存的对象。
解决方法:
1 2 3 4 5 6 7 8
| def counts(): sums = 0 def compute(): nonlocal sums sums += 1 print(sum) return compute() counts()
|
**注: **添加自由变量后,就可以将sums绑定为自由变量,具备自由变量特性。
什么是装饰器?
是可调用的对象(一般通过实现类的__call__方法),其参数是另一个函数(被装饰的函数),装饰器可能会处理被装饰的函数,可能会返回被装饰的函数,也可能会返回另一个函数或者可调用的对象。
怎么使用装饰器?
不带参数的装饰器
首先举个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| def counts(func): sums = 0 print('what is your name?') def compute(name): nonlocal sums sums += 1 func(name) print('sums:%d' % sums) return compute
@counts def test(name): print('my name is %s'% name) test('syz')
|
输出结果:
1 2 3
| what is your name ? # 运行时导入 my name is syz sums:1
|
首先介绍下装饰器运行时和导入时的区别:
在导入模块时,首先执行装饰器函数,而被装饰器装饰的函数或者装饰器返回的函数只在运行时调用,这也就是通常所说的导入时和运行时的区别。
test('syz')
<==>(等价于) counts(test)('syz')
带参数的装饰器
接下来进阶一下,假设我们一个函数要根据不同的参数执行相应的业务逻辑(可能不会调用这个函数本身,也可能调用另外的函数),这时候我们就需要一个带参数的装饰器了。
先看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| def aim_or_wholes(choice=True): def decorate(func): def inner(num1,num2,**m): print(num1,num2,m) if choice: return '6666' else: m = func(3,4) return m return inner return decorate
m = {'name':'syz'}
@aim_or_wholes(False) def test(num1,num2,**m): return num1+num2
test(num1=1,num2=3,m=4)
|
输出结果:
注:
① **
表示对字典拆包
② 通过三层函数组建的装饰器可以称之为装饰器工厂函数,可以表示带参数的装饰器,两层函数组键的装饰器为普通装饰器。
类装饰器
类装饰器相对于函数装饰器的一大优点就是好拓展。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import time import functools class T1: def __init__(self, name): self.name = name def __call__(self, func): print('I am outer!') @functools.wraps(func) def dec(*args, **kwargs): t0 = time.time() result = func(*args, **kwargs) elapsed = time.time() - t0 name = func.__name__ arg_lst = [] if args: arg_lst.append(', '.join(repr(arg) for arg in args)) if kwargs: pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())] arg_lst.append(', '.join(pairs)) arg_str = ', '.join(arg_lst) print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result)) return dec
@T1('test') def t2(): l = [1, 2, 3] ff = [x*x for x in l] print(ff)
t2()
|
结果:
1 2 3 4 5
| I am outer! [1, 4, 9] [0.00000000s] t2() -> None
|
注:
① 两层函数+一层类体或三层函数可以构成装饰器工厂函数,装饰器工厂函数说白了就是带参数的装饰器,装饰器工厂函数返回的是真正的装饰器。
② t2()
<=>(等价于)T1('test')(t2)()
③ 这里使用了__call__
函数,而T1('test')
<=>obj
,这样就转变为obj(t2)()
,是不是很眼熟,没错,这个就是函数的调用方式,秘诀就在于__call__
函数,可以让类实例函数化。
最后再来一个我的查重项目中的一个带参数装饰器,被装饰的函数在类中定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
def aim_or_whole(choice=True): pattern = get_content_regex(False)
def decorate(func): @functools.wraps(func) def inner(obj, row=None, whole=choice): global dict_context if choice: message = '全体匹配' dict_context = func(obj, row, True) sums = 0 for key, values in dict_context.items(): for value in values: value = re.sub(pattern, '', value) sums += len(value) return sums else: message = '主体匹配' pass
return inner
return decorate
|
注:
这里需要关注的是, def inner(obj, row=None, whole=choice):
中需要多一个obj实例,相当于self,func(obj, row, True)
同样func也需要。因此我们可以总结一下,如果参数不需要动态变化(*args,**kwargs
),那么装饰器函数中的函数的参数最好被装饰的函数的参数一致。