Python 装饰器闭包详解(看这一篇就够了)

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):  # 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): # 带默认值的参数True
def decorate(func): # 传入被装饰的函数
def inner(num1,num2,**m): # 接收被装饰函数的参数
print(num1,num2,m) # 可以看一下传递过来的参数
#print(args,kwargs)
#print(kwargs.pop('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
1 3 {'m': 4}
7

注:

**表示对字典拆包

② 通过三层函数组建的装饰器可以称之为装饰器工厂函数,可以表示带参数的装饰器,两层函数组键的装饰器为普通装饰器。


类装饰器

类装饰器相对于函数装饰器的一大优点就是好拓展。

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) # 这个装饰器用户获取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):
# 因为被装饰函数在类中定义,所以inner需要额外一个参数作为类实例obj
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 # 自定义choice = True时的情况

return inner

return decorate

注:

这里需要关注的是, def inner(obj, row=None, whole=choice):中需要多一个obj实例,相当于self,func(obj, row, True)同样func也需要。因此我们可以总结一下,如果参数不需要动态变化(*args,**kwargs),那么装饰器函数中的函数的参数最好被装饰的函数的参数一致。