Python进阶之闭包和装饰器
Walter Lv1

返回函数

我们来实现一个可变参数的求和。通常情况下,求和的函数是这样定义的:

1
2
3
4
5
def calc_sum(*args):
ax = 0
for n in args:
ax = ax + n
return ax

但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:

1
2
3
4
5
6
7
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum

当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数:

1
2
3
>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>

在上面的例子中,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种结构就是所谓的闭包。

每次调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数:

1
2
3
4
>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1==f2
False

f1()f2()的调用结果互不影响。

变量作用域

通常,函数内部的变量无法被函数外部访问,但内部可以访问;类内部的变量无法被外部访问,但类的内部可以。通俗来讲,就是内部代码可以访问外部变量,而外部代码通常无法访问内部变量。

Python的作用域一共有4层,分别是:

  • L (Local) 局部作用域
  • E (Enclosing) 闭包函数外的函数中
  • G (Global) 全局作用域
  • B (Built-in) 内建作用域
1
2
3
4
5
6
7
x = int(2.9)  # 内建作用域,查找int函数

global_var = 0 # 全局作用域
def outer():
out_var = 1 # 闭包函数外的函数中
def inner():
inner_var = 2 # 局部作用域

Python以**L –> E –> G –>B**的规则查找变量,即:在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,最后去内建中找。如果这样还找不到,那就提示变量不存在的错误。

  • 全局变量定义在全局作用域,是在整个程序中可以访问的变量
  • 局部变量定义在函数内部,是在特定的作用域中可以访问的变量
  • 自由变量是未在局部作用域中绑定的变量。

观察下面的代码

1
2
3
4
5
6
7
8
>>> b = 6
>>> def f1(a):
... print(a)
... print(b)
...
>>> f1(3)
3
6

函数体外的b为全局变量,函数体内的b为自由变量。因为自由变量b绑定到了全局变量,所以在函数f1()中能正确打印。

1
2
3
4
5
6
7
8
9
10
11
12
>>> b = 6	# 此处 b 为全局变量
def f1(a):
... print(a)
... print(b) # 此处b 为自由变量
... b = 9 # 此处b 绑定为局部变量
...
>>> f1(3)
3
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "<input>", line 3, in f1
UnboundLocalError: local variable 'b' referenced before assignment

由于Python不要求声明变量,而是假定在函数定义体中赋值的变量是局部变量,b 被声明为局部变量后,解释器便不会向下层级寻找全局作用域中的 b = 6

如果想让解释器把b当做全局变量,那么需要使用global声明:

1
2
3
4
5
6
7
8
9
10
>>> b = 6
>>> def f1(a):
... global b
... print(a)
... print(b)
... b = 9
...
>>> f1(3)
3
6

闭包

假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均值;例如,整个历史中某个 商品的平均收盘价。每天都会增加新价格,因此平均值要考虑至目前为止所有的价格。

使用类实现的话:

1
2
3
4
5
6
7
8
9
10
11
12
class Averager():
def __init__(self):
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total / len(self.series)

avg = make_averager()
avg(10) # 10.0
avg(11) # 10.5
avg(12) # 11.0

类实现不存在自由变量问题,因为self.series是类属性。

使用函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def make_averager():
series = []

def averager(new_value):
# series是自由变量
series.append(new_value)
total = sum(series)
return total / len(series)

return averager

avg = make_averager()
avg(10) # 10.0
avg(11) # 10.5
avg(12) # 11.0

调用 make_averager 时,返回一个 averager 函数对象。每次调用 averager 时,它会 把参数添加到系列值中,然后计算当前平均值。

那么问题来了,调用完make_averager()然后 return 之后,make_averager 的局部作用域已经丢失,为什么之后 avg(10)依然能在内层函数使用 series呢?

正是由 Python 中神奇的闭包特性实现:

image

series 在外层函数声明,在内层被绑定在average()的局部作用域上 。

1
2
3
4
5
6
7
8
>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__ # series 的绑定在返回的 avg 函数的 __closure__ 属性中
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]

综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。

nonlocal

在上面的例子中我们使用List 保存历史值, 如果只存储目前的总值和元素个数,然后使用这两个数计算均值:

1
2
3
4
5
6
7
8
9
10
11
def make_averager():
count = 0
total = 0
def averager(new_value):
count += 1
total += new_value
return total / count
return averager

avg = make_averager()
avg(10) # UnboundLocalError: local variable 'count' referenced before assignment

问题是,当 count 是数字或任何不可变类型(向新对象赋值)时,count += 1 语句的作用其实与 count = count + 1 一样。因此,我们在 averager 的定义体中为 count 赋值了,这会把 count 变成局部变量。total 变量也受这个问题影响。

但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例 如 count = count + 1,其实会隐式创建局部变量 count。这样,count 就不是自由变量 了,因此不会保存在闭包中。

Python 3 引入了 nonlocal 声明。它的作用是把变量标记为自由变量, 即使在函数中为变量赋予新值了,也会变成自由变量。如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。

1
2
3
4
5
6
7
8
9
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager

装饰器

装饰器即在不改动原函数功能的要求下,增加原函数的功能

有以下口诀:客人空手来,还带请上楼(在内层函数添加功能),干啥都同意(执行原函数功能),有参给上楼

编写一个装饰器用来统计函数的运行时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 以下就是一个简单的装饰器函数
def clock(func):
def cloked():
start = time.time()
func() # 执行原函数的功能
end = time.time()
print(end-start)
return cloked

def orign():
print("hahahahahaha")

f2 = clock(orign)
f2()
# hahahahahaha
# 1.0013580322265625e-05

这行代码之所以可用,就是因为 clocked 函数的闭包中包含 func 自由变量。

功能已经完成了,但是这样写还是太麻烦。

Python中有一个语法糖:在原函数定义的上方加上@装饰器函数名称,之后再使用orgin()就等同于执行以上两句。

1
2
3
4
5
@clock
def orign():
print("hahahahahaha")

orign()

那么原函数中如果带有参数怎么办呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def metric(func):
def wrapper(*args):
start = time.time()
func(*args) # 执行原函数功能
end = time.time()
print(f"{func.__name__} runed {end-start:.8f}s")
return wrapper


# 演示一个带参数的原函数
@metric
def print_age(name, age):
print(f'{name}今年{age}岁')

print_age('小明',18)
# 小明今年18岁
# print_age runed 0.00001097s

需要传入参数的装饰器

假如我们所定义的装饰器需要传入参数,该怎么办呢?

答案是在装饰器函数增加返回函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 定义一个装饰器,在函数执行前打印日志
import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args,**kwargs):
print(f'{text}{func.__name__}')
return func(*args, **kwargs)
return wrapper
return decorator

@log('执行函数 ')
def print_age(name,age):
print(f'{name}今年{age}岁')

print_age("wang",23)
# 执行函数 print_age
# wang今年23岁

@语法糖等同于以下三句

1
2
3
deco = log("执行函数 ")
f = deco(print_age)
f('wang',22)

@functools.wraps(func)效果等同于wrapper.__name__ = func.__name__,防止有些依赖函数签名的代码执行就会出错。

可选参数的装饰器

如何定义一个装饰器,使其参数可选

如:

  • @log
  • @log(‘exe’)

仍以上面打印日志的装饰器为例

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
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if type(text) is str:
print(f'{text} {func.__name__}')
else:
print(f'执行了 {func.__name__}')
return func(*args,**kwargs)
return wrapper

if type(text) is str: # 常规的带参数
return decorator
else: # 不带参数
return decorator(text) #此处text形参实际上是传入func

@log('exe')
def say_hello(word):
print(f'hello {word}!')

@log
def say_hi(word):
print(f'hi {word}!')

say_hello("wang")
say_hi("小明")

# exe say_hello
# hello wang!
# 执行了 say_hi
# hi 小明!

@语法糖等同于

1
2
log('exe')(say_hello)("wang")
log(say_hi)("小明") # 注意这里的log中的形参text实际上是传入了func

参考:

《流畅的Python》

刘江的博客

廖雪峰的博客

 评论