浅谈 Python 装饰器

Python 装饰器

何为闭包(Closure)

Python 作用域(LEGB)

  • L (Local): 局部作用域 , 作用域范围最小, 是函数内部的作用域
  • E(Enclosing): 嵌套作用域, 适用于嵌套函数, 即父级函数的局部作用域
1
2
3
4
5
def father():
father_name = 'father' # 父级函数作用域
def children():
child_name = 'children'
print (father_name, child_name) # 子函数可以访问父级函数作用域内的变量
  • G(Global): 全局作用域, 全局作用域范围仅限于单个文件
  • B(build-in): 内置作用域, 系统内模块的变量
1
2
3
4
5
6
7
import os  # 内置作用域
g = 1 # 全局作用域
def father():
father_name = 'father' # 父级函数作用域
def children():
child_name = 'children'
print (father_name, child_name) # 子函数可以访问父级函数作用域内的变量

python 对变量名的解析顺序为 LEGB, 因此对于分别处于不同级别的作用域中,若出现相同的变量,则会出现Python只认最先找到的变量的值

1
2
3
4
5
6
7
8
foo = 'global value'
def fir():
foo = 'enclosing value'
str = 'i am just a str not func'
def second():
foo = 'local value'
print (foo) # 输出 local value
print str(1) # 会报错,因为str不再是Python内置函数,而只是一个字符串

闭包

闭包 就是延伸了作用域的函数, 装饰器的实现依靠于闭包这个特性, 闭包主要体现在函数式编程上

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
# 实现一个简单的求均值函数
glb = 999
def average():
total = [] # 可变对象 --> 这是重点
def calc(a):
total.append(a)
print (total)
print (glb)
ave = sum(total)/len(total)
return ave
return calc
if __name__ == '__main__':
ave = average() # a
print (ave(5)) # b
print (ave(6))
print (ave(7))

'''
# -----------结果--------------
name: demo
[5]
5.0
[5, 6]
5.5
[5, 6, 7]
6.0
>>>

函数式编程的一个特点是允许把函数本身当做参数传入另一个函数, 又或者返回一个函数 —摘自廖雪峰教程

为何函数式编程需要闭包?

就我个人的理解, 重点是这句话函数式编程一般返回的是一个函数, 函数是一个对象,这是个重点!!!, 而且这个对象是个可调用对象(具有__call__ 方法).

在上方的示例代码中,

  • a 行代码所做工作如下:
    • 将calc 函数对象赋值给ave, 因此 ave(a) 等同于 calc(a)
  • b 行代码所做工作如下:
    • ave(5) 等同于 calc(5), 其效果相当于执行一遍calc函数的代码,并将结果返回
    • 这时就有个问题, average()只是将calc函数对象赋值ave,对于ave而言,其按道理是无法访问在calc函数外部的变量total, 因为total并没有返回给ave
    • 为了解决这类问题,便引入了闭包的概念

闭包的作用?

闭包是延伸了作用域的函数,可以这么理解, 由于闭包的机制, calc的作用域被延伸了, 简单的理解,就是闭包的作用就是将某个函数的作用域延伸到只有这个函数也能正常运行的程度!!, 由于对于全局变量(G)和Python内置作用域(B),即使没有闭包机制,ave还是可以访问, 因此一般而言闭包的作用是将被高阶函数返回的函数的作用域延伸到嵌套作用域(E)

闭包是如何进行作用域的延伸吗?

函数作为一个对象,其具有一个称为自由变量(free variable)的属性, 那些属于本地作用域之外的变量(L),属于嵌套作用域(E)的变量,都以自由变量的形式存在着

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
glb = [] # 全局作用域 G
def average():
total = [] # 嵌套作用域 E
def calc(a):
total.append(a)
glb.append(a)
print ('total: {}, glb: {}'.format(total, glb))
ave = sum(total)/len(total)
return ave
return calc
if __name__ == '__main__':
ave = average()
print ('ave 的局部变量名: {}'.format(ave.__code__.co_varnames))
print ('ave 的自由变量名: {}'.format(ave.__code__.co_freevars))
print ('ave(5): {}'.format(ave(5)))
print ('ave(6): {}'.format(ave(6)))
print ('ave(7): {}'.format(ave(7)))
print ('ave 的自由变量total的值: {}'.format(ave.__closure__[0].cell_contents))

'''
# ---------------结果------------------
ave 的局部变量名: ('a', 'ave')
ave 的自由变量名: ('total',)
total: [5], glb: [5]
ave(5): 5.0
total: [5, 6], glb: [5, 6]
ave(6): 5.5
total: [5, 6, 7], glb: [5, 6, 7]
ave(7): 6.0
ave 的自由变量total的值: [5, 6, 7]

由上方示例代码可知, 全局变量并没有加到自由变量中, 因为没必要! 无论有没有闭包, 全局变量和Python内置变量都可以访问,

可以通过ave.__code__.co_freevars来获得自由变量名字, 通过ave.__closure__[index].cell_contents来获得自由变量的值

总结

闭包没有想象中的难理解, 闭包的作用的延伸函数的作用域, 而延伸的方式是将不在作用域范围内的变量(E作用域)储存到由高阶函数返回的函数对象的自由变量中

闭包中的一些坑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 实现一个求均值函数
def average():
total = 0
num = 0
def calc(a):
# nonlocal total, num # 声明为自由变量(free variable), 适用于python3
total += a
num += 1
ave = total/num
return ave
return calc

if __name__ == '__main__':
ave = average()
ave(5)
'''
# 将会报错 --> UnboundLocalError: local variable 'total' referenced before assignment

对于上方示例代码, 报错原因是因为total, num 是不可变对象, 在calc函数内, 对着两个自由变量进行了加法并赋值操作, 实际上,在calc函数内部, total,和num被赋值为新的对象,新的对象不再是自由变量,而是局部变量, 因此被average函数返回的calc函数对象,并没有延伸作用域, 所以会出现报错信息

为何之前的代码没有报错?

因为,之前作为自由变量的对象是可变对象, 因此,对可变对象进行加法,赋值等操作并不会改变这个对象, 它仍然是个自由变量

如何解决?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 对于Python2
class Namespace:
pass

# 实现一个求均值函数
def average():
ns = Namespace()
ns.total = 0
ns.num = 0
def calc(a):
ns.total += a
ns.num += 1
print (ns.total, ns.num)
ave = ns.total/ns.num
return ave
return calc
1
2
3
4
5
6
7
8
9
10
11
12
# 对于Python3
# 实现一个求均值函数
def average():
total = 0
num = 0
def calc(a):
nonlocal total, num # 声明为自由变量(free variable), 适用于python3
total += a
num += 1
ave = total/num
return ave
return calc

Python3 中新引入了一个nonlocal 声明, 当对某个自由变量进行声明时, 即使在函数中,对自由变量赋新值,也会变成自由变量

装饰器

了解了闭包的原理后,装饰器就很好理解了, 装饰器顾名思义就是对函数进行包装修饰,并返回被修饰后的函数

不带参数的装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def decorate(func):
def wrapper(*args, **kwargs):
print ('我正在装饰函数...')
return func(*args, **kwargs)
return wrapper

@decorate
def add(a,b):
print ('我在执行add函数功能 计算结果为: {}'.format(a+b))
return a+b

print (add(3,4))

''''
# ---------结果--------------

我正在装饰函数...
我在执行add函数功能 计算结果为: 7
7

不用装饰器的实现方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def decorate(func):
def wrapper(*args, **kwargs):
print ('我正在装饰函数...')
return func(*args, **kwargs)
return wrapper

def add(a,b):
print ('我在执行add函数功能 计算结果为: {}'.format(a+b))
return a+b

add = decorate(add)
print (add(3,4))

''''
# ----------------结果---------------
我正在装饰函数...
我在执行add函数功能 计算结果为: 7
7

这边主要有几个点需要注意:

  • 装饰器传入的是函数

  • 返回的也是一个函数

  • @decorate 实际上做了这么一件事:

    1
    add = decorate(add)

    即将所装饰的函数add做为参数,传入decorate()函数中,并将所装饰后的函数传给add这个变量,

    add(3,4) 等同于wrapper(3,4) , 而这样不会报错主要是归功于闭包机制

带参数的修饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 三层函数,多出来的函数用来接收装饰器的参数
def decoration(arg):
def decorate(func):
def wrapper(*args, **kwargs):
print ('我正在装饰函数...')
print ('我正在使用修饰器传来的参数: {}'.format(arg))
return func(*args, **kwargs)
return wrapper
return decorate

@decoration(666)
def add(a,b):
print ('我在执行add函数功能 计算结果为: {}'.format(a+b))
return a+b
add(3,4)

''''
# -----------结果--------------
我正在装饰函数...
我正在使用修饰器传来的参数: 666
我在执行add函数功能 计算结果为: 7

将修饰传入的参数,放入被修饰的函数内部使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def decoration(arg):
def decorate(func):
def wrapper(*args, **kwargs):
print ('我正在装饰函数...')
# print ('我正在使用修饰器传来的参数: {}'.format(arg))
return func(arg, *args, **kwargs) # 将传入的参数 作为func的第一个参数
return wrapper
return decorate

@decoration(666)
def add(arg, a,b):
print ('我正在函数内部使用修饰器传来的参数: {}'.format(arg))
print ('我在执行add函数功能 计算结果为: {}'.format(a+b))
return a+b
add(3,4)

''''
# --------------结果-----------------
我正在装饰函数...
我正在函数内部使用修饰器传来的参数: 666
我在执行add函数功能 计算结果为: 7

本文标题:浅谈 Python 装饰器

文章作者:定。

发布时间:2017年11月6日 - 01时11分

本文字数:6,256字

原始链接:http://cocofe.cn/2017/11/06/浅谈 Python 装饰器/

许可协议: Attribution-NonCommercial 4.0

转载请保留以上信息。