Fluent Python 笔记 —— 装饰器和闭包

本文详细探讨了Python中的装饰器和闭包。装饰器在代码中用于增强函数行为,它在函数定义后立即执行。通过案例分析了装饰器何时执行以及如何处理变量作用域。闭包则是具有自由变量的函数,可以保留定义时的作用域。文中还介绍了如何创建参数化的装饰器,并展示了如何利用`functools.singledispatch`实现单分派泛函数,提高代码的灵活性和可扩展性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。它是一种以另一个函数(被装饰的函数)为参数的可调用对象,可能会处理被装饰的函数并将其返回,或者将其替换为另一个函数。

装饰器严格来说只是语法糖。假如有个名为 decorate 的装饰器:

@decorate
def target():
print(‘running target()’)

上述代码效果等同于如下写法:

def target():
print(‘running target()’)

target = decorate(target)

即原来的 target 函数会被替换为 decorate(target) 返回的函数。

def deco(func):
… def inner():
… print(‘running inner()’)
… return inner

@deco
… def target():
… print(‘running target()’)

target()
running inner()

target
<function deco..inner at 0x7ff01bad9a60>

如上述代码,deco 返回 inner 函数对象,使用 deco 装饰 target,调用被装饰的 target 实际会运行 inner。target 对象变为 inner 的引用。

装饰器有如下两大特性:

能把被装饰的函数替换成其他函数
装饰器在加载模块时立即执行

装饰器何时执行

装饰器会在被装饰的函数定义之后立即运行,这通常是在 Python 加载模块时。

参考如下 registration.py 模块:

registry = []

def register(func):
print(f’running register({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 0x7fbc852d43a0>)
running register(<function f2 at 0x7fbc852d4430>)
running main()
registry -> [<function f1 at 0x7fbc852d43a0>, <function f2 at 0x7fbc852d4430>]
running f1()
running f2()
running f3()

Python 加载模块后,装饰器 register 会在其他函数之前运行,将被装饰的函数(f1 和 f2)的引用添加到 registry 列表中。原本的函数 f1 和 f2,以及未被装饰的 f3,则只在 main 明确调用它们时才执行。

如果导入 registration.py 模块(不作为脚本运行),输出如下:

import registration
running register(<function f1 at 0x7f8fbd8c3b80>)
running register(<function f2 at 0x7f8fbd8c3c10>)

函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时执行。突出了导入时和运行时之间的区别。
变量作用域规则

测试如下函数,它读取两个变量的值,一个是局部变量 a,是函数的参数;另一个是未被定义的变量 b:

def f1(a):
… print(a)
… print(b)

f1(3)
3
Traceback (most recent call last):
File “”, line 1, in
File “”, line 3, in f1
NameError: name ‘b’ is not defined

运行时变量 a 的值正常输出,接着报出 name ‘b’ is not defined。

若先给全局变量 b 赋值,再调用 f1 函数,就不会报错:

b = 6
f1(3)
3
6

但如下代码的结果可能会让人意想不到:

b=6
def f2(a):
… print(a)
… print(b)
… b = 9

f2(3)
3
Traceback (most recent call last):
File “”, line 1, in
File “”, line 3, in f2
UnboundLocalError: local variable ‘b’ referenced before assignment

代码运行后首先输出了 3( print(a) ),但是第二个语句 print(b) 执行报错。按照直觉第二个 print 语句应该输出 6,因为全局变量 b 已经在函数执行之前赋值,局部变量 b 的赋值动作也是在 print 语句后面。

事实上是,Python 在编译函数定义体时,会判断 b 是局部变量,Python 会尝试从本地环境获取 b。调用 f2(3) 时,f2 的定义体尝试获取局部变量 b 的值,发现 b 没有绑定后报错。

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

b = 6
def f3(a):
… global b
… print(a)
… print(b)
… b = 9

f3(3)
3
6

b
9

f3(3)
3
9

b = 30
b
30

闭包

闭包指延伸了作用域的函数,其中包含函数定义体中引用、不在定义体中定义的非全局变量。

计算移动平均值(不断增加的系列值的均值)的类:

average_oo.py

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)

效果如下:

from average_oo import Averager
avg = Averager()
avg(10)
10.0

avg(11)
10.5

avg(12)
11.0

以下代码是同样功能的函数式实现:

def make_averager():
series = []

def averager(new_value):
    series.append(new_value)
    total = sum(series)
    return total / len(series)

return averager

from average import make_averager
avg = make_averager()
avg(10)
10.0

avg(11)
10.5

avg(12)
11.0

第一个例子中,Averager 类的实例 avg 存储历史值的位置很明显:通过 self.series 实例属性。

第二个例子中, series 是 make_averager 函数的局部变量,但调用 avg(10) 时,make_averager 函数已经返回,它的本地作用域也就不存在了。

在 averager 函数中,series 是自由变量(free variable),指未在本地作用域中绑定的变量。在这里插入图片描述

闭包是一种函数,它会保留定义函数时存在的自由变量的绑定。这样在调用函数时,即便定义作用域不可用了,通过闭包仍能使用那些绑定。
nolocal

前面实现 make_averager 函数的方式效率并不高,把所有值存储在历史列表中,在每次调用 averager 时使用 sum 求和。更好的实现方式是,只存储目前的总和以及元素个数,只使用这两个值计算均值。

average2.py

def make_averager():
count = 0
total = 0

def averager(new_value):
    count += 1
    total += new_value
    return total / count

return averager

上述代码运行后会报出如下错误:

from average2 import make_averager
avg = make_averager()
avg(10)
Traceback (most recent call last):
File “”, line 1, in
File “/home/starky/program/python/algorithm/average2.py”, line 6, in averager
count += 1
UnboundLocalError: local variable ‘count’ referenced before assignment

原因在于,当 count 是数字或其他任何不可变类型时, count += 1 的作用等同于 count = count + 1 。导致在 averager 的定义体中为 count 赋值了,将 count 变成了局部变量。 total 变量也是如此。

之前的 series 变量没有出现此问题,原因是只调用了 series.append ,列表作为可变对象,并不存在重新赋值的情况。

Python 3 中引入了 nolocal 声明,其作用是把变量标记为自由变量。为 nolocal 声明的变量赋予新值后,闭包中保存的绑定也会更新。

def make_averager():
count = 0
total = 0

def averager(new_value):
    nonlocal count, total
    count += 1
    total += new_value
    return total / count

return averager

实现一个简单的装饰器

import time

def clock(func):
def clocked(*args):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.name
arg_str = ‘, ‘.join(repr(arg) for arg in args)
print(’[%0.8fs] %s(%s) -> %r’ % (elapsed, name, arg_str, result))
return result
return clocked

@clock
def snooze(seconds):
time.sleep(seconds)

@clock
def factorial(n):
return 1 if n < 2 else n * factorial(n - 1)

if name == ‘main’:
print(’’ * 40, ‘Calling snooze(.123)’)
snooze(.123)
print(’
’ * 40, ‘Calling factorial(6)’)
print(‘6! =’, factorial(6))

执行结果如下:

**************************************** Calling snooze(.123)
[0.12318703s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000168s] factorial(1) -> 1
[0.00003647s] factorial(2) -> 2
[0.00006038s] factorial(3) -> 6
[0.00008216s] factorial(4) -> 24
[0.00010411s] factorial(5) -> 120
[0.00012920s] factorial(6) -> 720
6! = 720

在上述代码中:

@clock
def factorial(n):
return 1 if n < 2 else n * factorial(n - 1)

等同于:

def factorial(n):
return 1 if n < 2 else n * factorial(n - 1)
factorial = clock(factorial)

factorial 会作为 func 参数传递给 clock,返回 clocked 函数。Python 解释器在背后会把 clocked 赋值给 factorial。此后,每次调用 factorial(n) ,实际执行的都是 clocked(n) 。总体步骤如下:

记录初始时间 t0
调用原来的 factorial 函数,保存结果
计算执行的时间
格式化收集到的数据
返回第二步保存的结果

以上即装饰器的典型行为:将被装饰的函数替换为新函数,二者接收同样的参数,(通常)返回被装饰函数本该返回的值,并做些额外的操作。
参数化装饰器
参数化的注册装饰器

为了便于启用或禁用 register 的函数注册功能,可以为其提供一个可选的 active 参数,设为 False 时,不注册被装饰的函数。

从概念上讲,这个新的 register 函数不是装饰器,而是装饰器工厂函数,用来返回真正的装饰器。

registration_param.py

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()’)

运行效果:

import registration_param
running register(active=False)->decorate(<function f1 at 0x7fa801b1bc10>)
running register(active=True)->decorate(<function f2 at 0x7fa801b1bca0>)

registration_param.registry
{<function f2 at 0x7fa801b1bca0>}

decorate 是装饰器,必须返回一个函数。register 是装饰器工厂函数,返回 decorate。

只有 active 参数的值为 True 时才注册 func;若 active 不为真,且 func 在 registry 中,则将 func 移除。

@register 工厂函数必须作为函数调用,传入所需参数(或 @register() )。

若不使用 @ 句法,也可以像常规函数那样使用 register:

from registration_param import *
running register(active=False)->decorate(<function f1 at 0x7fc32e6d0b80>)
running register(active=True)->decorate(<function f2 at 0x7fc32e6d0c10>)

registry
{<function f2 at 0x7fc32e6d0c10>}

register()(f3)
running register(active=True)->decorate(<function f3 at 0x7fc32e6d0af0>)
<function f3 at 0x7fc32e6d0af0>

registry
{<function f2 at 0x7fc32e6d0c10>, <function f3 at 0x7fc32e6d0af0>}

register(active=False)(f2)
running register(active=False)->decorate(<function f2 at 0x7fc32e6d0c10>)
<function f2 at 0x7fc32e6d0c10>

registry
{<function f3 at 0x7fc32e6d0af0>}

参数化的 clock 装饰器

clock_param.py

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)

=> [0.12320948s] snooze(0.123) -> None

=> [0.12319684s] snooze(0.123) -> None

=> [0.12318802s] snooze(0.123) -> None

clock_param2.py

import time
from clock_param import clock

@clock(’{name}({args}) dt={elapsed:0.3f}s’)
def snooze(seconds):
time.sleep(seconds)

for i in range(3):
snooze(.123)

=> snooze(0.123) dt=0.123s

=> snooze(0.123) dt=0.123s

=> snooze(0.123) dt=0.123s

标准库中的装饰器——单分派泛函数

假设需要开发一个调试 Web 应用的工具,能够生成 HTML 来显示不同类型的 Python 对象。

import html

def htmlize(obj):
content = html.escape(repr(obj))
return ‘

{}
’.format(content)

上述函数适用于任何 Python 类型。但如果想做进一步扩展,使其能够用不同的方式显示不同的类型:

str:把内部换行符替换为 ‘\

\n’,使用

进行格式化
int:以十进制和十六进制显示数字
list:输出 HTML 列表,根据各个元素的类型进行格式化

Python 不支持重载方法或函数,因此不能使用不同的签名定义 htmlize 的变体,也无法使用不同的方式处理不同的数据类型。

一种常见的做法是将 htmlize 变成一个分派函数,使用一系列 if/elif/elif 调用专门的函数,如 htmlize_str、htmlize_int 等。但这样不便于模块的扩展,且显得笨拙。分派函数 htmlize 会随着时间推移变得很大,与各个专门函数之间的耦合也很紧密。

Python 中的 functools.singledispatch 装饰器可以把整体方案拆分为等多个模块,甚至可以为无法修改的类提供专门函数。使用 @singledispatch 装饰的普通函数会变成泛函数(generic function),根据第一个参数的类型以不同方式执行相同操作的一组函数。

htmlize.py

from functools import singledispatch
from collections import abc
import numbers
import html

@singledispatch
def htmlize(obj):
content = html.escape(repr(obj))
return ‘

{}
’.format(content)

@htmlize.register(str)
def _(text):
content = html.escape(text).replace(’\n’, ‘
\n’)
return ‘

{0}

’.format(content)

@htmlize.register(numbers.Integral)
def _(n):
return ‘

{0} (0x{0:x})
’.format(n)

@htmlize.register(tuple)
@htmlize.register(abc.MutableSequence)
def _(seq):
inner = ‘\n

  • ’.join(htmlize(item) for item in seq)
    return ‘
    • \n
    • ’ + inner + ‘
    • \n
  • from htmlize import htmlize
    htmlize({1, 2, 3})

    {1, 2, 3}

    htmlize(abs)

    htmlize(‘Heimlich & Co.\n- a game’)

    Heimlich & Co.
    \n- a game

    htmlize(42)

    42 (0x2a)

    print(htmlize([‘alpha’, 66, {3, 2, 1}]))

    • alpha

    • 66 (0x42)
    • {1, 2, 3}

    @singledispatch 标记处理 object 类型的基函数。各个专门函数使用 @<base_function>.register() 装饰。

    为每个需要特殊处理的类型注册一个函数, numbers.Integral 是 int 的抽象基类。只要可能,注册的专门函数应该尽量处理抽象基类(如 numbers.Integral 和 abc.MutableSequence ),不要处理具体实现(如 int 和 list )。这样代码支持的兼容类型会更广泛(支持抽象基类现有的和未来的具体子类),比如用户可能通过子类化 numbers.Integral 实现固定位数的 int 类型。

    可以叠放多个 register 装饰器,让同一个函数支持不同类型。

    @singledispatch 可以在系统的任何地方和任何模块中注册专门函数,还可以为不是自己编写的或者不能修改的类添加自定义函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值