Python 函数装饰器和闭包

本文最后更新于:2022年7月4日 上午

函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为, 是一项强大的功能。本文记录相关内容。

装饰器基础知识

定义方式

  • 装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个 函数或可调用对象。
1
2
3
4
5
6
7
8
@decorate 
def target():
print('running target()')

--------------------------------------------
def target():
print('running target()')
target = decorate(target)
  • 两种写法的最终结果一样:上述两个代码片段执行完毕后得到的 target 不一定是原来那个 target 函数,而是 decorate(target) 返 回的函数。

  • 装饰器只是语法糖,装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。

    有时,这样做更方便,尤其是做元编程(在运行时改变程序的行为)时。

  • 装饰器的一大特性是,能把被装饰的函数替换成其他函数。第二个特性是,装饰器在加载模块时立即执行。

执行装饰器

  • 装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即 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
    28
    29
    30
    31
    32
    33
    34
    35
    36
    registry = []
    def register(func):
    print('running register(%s)' % func)
    registry.append(func)
    return func

    @register
    def f1():
    print('running f1()')

    @register
    def f2():
    print('running f2()')

    def f3():
    print('running f3()')

    def main():
    print('running main()')
    print('registry ->', registry)
    f1()
    f2()
    f3()

    if __name__=='__main__':
    main()


    -->
    running register(<function f1 at 0x0000021C4D7C68B0>)
    running register(<function f2 at 0x0000021C4D7C6820>)
    running main()
    registry -> [<function f1 at 0x0000021C4D7C68B0>, <function f2 at 0x0000021C4D7C6820>]
    running f1()
    running f2()
    running f3()
  • running register 是在 main 函数运行前执行的。函数装饰器在导入模块时立即执行,而被装饰的 函数只在明确调用时运行。这突出了 Python 程序员所说的导入时和运行时之间的区别。

  • 示例的 register 装饰器原封不动地返回被装饰的函数,但是这种技术并非没有用处。很多 Python Web 框架使用这样的装饰器把函 数添加到某种中央注册处。

变量作用域

  • 函数在执行中,可以获取已经定义的函数外的全局变量:
1
2
3
4
5
6
7
8
9
def f1(a): 
print(a)
print(b)

f1(3)
b = 9

-->
Error: name 'b' is not defined

调用前未定义的全局变量会被认为未定义

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

b = 9
f1(3)

-->
3
9

调用前定义过的全局变量可以正常获取

1
2
3
4
5
6
7
8
9
10
def f2(a): 
print(a)
print(b)
b = 6

b = 9
f2(3)

-->
Error: local variable 'b' referenced before assignment

函数 f2f1 多了一行对 b 变量的赋值语句,使用相同的调用方式却会报错。

因为:Python 编译函数的定义体时,它判断 b 是局部变量,因为在函数中给它赋值了。

  • Python 的设计思路:Python 不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。

  • 如果在函数中赋值时想让解释器把 b 当成全局变量,要使用 global 声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def f3(a): 
global b
print(a)
print(b)
b = 6

b = 9
f3(3)
print(b)

-->
3
9
6

程序可以正常运行,内部变量为全局变量,并在函数内成功修改全局变量绑定关系

闭包

  • 闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是 不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是 它能访问定义体之外定义的非全局变量。

    有点绕,我们看一个例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    def make_averager(): 
    series = []
    def averager(new_value):
    series.append(new_value)
    total = sum(series)
    return total/len(series)
    return averager

    avg = make_averager()
    print(avg(5))
    print(avg(7))
    print(avg(9))

    -->
    5.0
    6.0
    7.0

    例子中可以看到,avg 使用 make_averager 的返回值做了平均值记录的工作,但是 make_averager 的生命周期应该早就结束了才对

  • 在 averager 函数中,series 是自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量:

    averager 的闭包延伸到那个函数的作用域之外,包含自由变量 series 的绑定

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

    注意:只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。

nonlocal

  • 上文中的 make_averager 函数每次计算均值时都要重新计算序列中所有元素的和,效率不高,直接保存总和和元素个数的策略在算法复杂度上更优,参考以下示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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(3)
    avg(5)
    avg(7)

    -->
    local variable 'count' referenced before assignment

    乍一看有些反直觉,事实上是没有问题的,+= 操作暗含了赋值操作,因此 python 会将 counttotal 认为是局部变量,因此不会按照我们设计函数的思路运行

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    def make_averager(): 
    count = 0
    total = 0
    def averager(new_value):
    nonlocal count, total
    count += 1
    total += new_value
    return total / count
    return averager

    avg = make_averager()
    print(avg(3))
    print(avg(5))
    print(avg(7))

    -->
    3.0
    4.0
    5.0

装饰器高级规则

叠放装饰器

  • d1d2 两个装饰器按顺序应用到 f 函数上,作用相当于 f = d1(d2(f))

  • 也就是说,以下两段代码等价:

1
2
3
4
@d1 
@d2
def f():
print('f')
1
2
3
def f(): 
print('f')
f = d1(d2(f))

参数化装饰器

解析源码中的装饰器时,Python 把被装饰的函数作为第一个参数传给装饰器函数。那怎么让装饰器接受其他参数呢?

创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。

举个栗子
  • 简单的装饰器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    registry = [] 
    def register(func):
    print('running register(%s)' % func)
    registry.append(func)
    return func

    @register
    def f1():
    print('running f1()')

    print('running main()')
    print('registry ->', registry)

    f1()
  • 参数化的注册装饰器

    • 为了便于启用或禁用 register 执行的函数注册功能,我们为它提供一个可选的 active 参数,设为 False 时,不注册被装饰的函数。
    • 从概念上看,这个新的 register 函数不是装饰器, 而是装饰器工厂函数。调用它会返回真正的装饰器,这才是应用到目标 函数上的装饰器。
    • 为了接受参数,新的 register 装饰器必须作为函数调用:
    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
    registry = set()
    def register(active=True):
    def decorate(func):
    print('running register(active=%s)->decorate(%s)' % (active, func))
    if active:
    registry.add(func)
    else:
    registry.discard(func)
    return func
    return decorate

    @register(active=False)
    def f1():
    print('running f1()')

    @register()
    def f2():
    print('running f2()')

    def f3():
    print('running f3()')

    print(register)


    -->
    running register(active=False)->decorate(<function f1 at 0x000001FB582C3940>)
    running register(active=True)->decorate(<function f2 at 0x000001FB582C3AF0>)
    <function register at 0x000001FB582C3A60>

    参数为 True 的被注册,False 的没有

再举个栗子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import time 

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
def clock(fmt=DEFAULT_FMT):
def decorate(func):
def clocked(*_args):
t0 = time.time()
_result = func(*_args)
elapsed = time.time() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args)
result = repr(_result)
print(fmt.format(**locals()))
return _result
return clocked
return decorate

if __name__ == '__main__':
@clock()
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
  • clock 装饰器中加入了 DEFAULT_FMT 参数
  • 由于装饰器是在模块加载时执行的,动态参数装饰函数比较困难

参考资料


Python 函数装饰器和闭包
https://www.zywvvd.com/notes/coding/python/fluent-python/chapter-7/python-decorate/python-decorate/
作者
Yiwei Zhang
发布于
2022年6月5日
许可协议