第九章 装饰器与闭包

第九章 装饰器与闭包

定义装饰器

装饰器是一个可调用对象(callable),接收另一个函数作为参数(即被装饰的函数)。装饰器可以对被装饰函数进行处理;返回原函数,或用另一个函数/可调用对象替换它。

装饰器本质

装饰器本质上是语法糖。在元编程(metaprogramming)等场景中,直接调用装饰器并传入函数有时更为灵活。使用 @decorator 语法糖等价于显式调用装饰器并重新赋值。

# 装饰器语法
@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
# 调用 target() 实际执行的是 inner 函数;
def target(): 
    print('running target()')

target()
# target 名称现在引用的是 deco 内部定义的 inner 函数。
print(target)
# 输出
# running inner() 
# <function deco.<locals>.inner at 0x...>

装饰器核心特性

  1. 装饰器本身是一个可调用对象(通常是函数);
  2. 可能替换被装饰的函数
  3. 在模块加载时立即执行(即装饰发生在导入时,而非调用时)。

何时执行装饰器

装饰器在被装饰函数定义之后立即执行。该过程发生在 模块加载时(import time),即 Python 导入模块的过程中。被装饰的函数本身不会立即执行,只有在被显式调用时才运行。这体现了 Python 中“导入时”(import time)与“运行时”(runtime)的关键区别。此机制常用于函数注册、插件系统、缓存初始化等场景。

registry = []  # 保存被 @register 装饰的函数引用

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()
# 作为脚本运行时
$ python3 registration.py
# register 在 main() 执行前已调用两次
# 装饰器接收到的是函数对象(如 <function f1 at 0x...>)
running register(<function f1 at 0x...>)
running register(<function f2 at 0x...>)
running main()
# registry 在模块加载完成后已包含 f1 和 f2 的引用
registry -> [<function f1 at 0x...>, <function f2 at 0x...>]
# 函数体(如 print('running f1()'))仅在 main() 中调用时才执行
running f1()
running f2()
running f3()
# 仅导入模块时
>>> import registration
# 即使不运行 main(),装饰器仍会在导入时执行
running register(<function f1 at 0x...>)
running register(<function f2 at 0x...>)
>>> registration.registry
# registry 已被填充,说明装饰器逻辑在导入阶段完成
[<function f1 at 0x...>, <function f2 at 0x...>]

@register装饰器在两个方面与实际用法有所不同

  1. 定义位置
    装饰器函数(register)与被装饰函数(f1, f2)定义在同一模块中。
    → 实际项目中,装饰器通常在一个模块中定义,然后在其他模块中使用

  2. 行为方式
    register 装饰器直接返回原函数,未做任何修改。
    → 大多数真实装饰器会定义一个内部函数(inner function)并返回它,从而替换原函数

虽然 register 没有修改被装饰函数,但“原样返回”是一种有效模式。广泛应用于框架中,用于将函数注册到中心注册表(registry)。典型场景如将 URL 路径映射到处理函数(如 Web 框架中的路由装饰器)。这类装饰器可能修改也可能不修改原函数,核心目的是注册

变量作用域

Python 函数在访问变量时遵循以下规则:

  • 局部变量:在函数参数或函数体内被赋值的变量。
  • 全局变量:在模块顶层(函数/类外部)定义的变量。
# 读取局部与全局变量
def f1(a):
    print(a)
    print(b)  # b 未在函数内赋值 → 视为全局变量
# 若全局未定义 b,调用 f1(3) 会抛出 NameError。

赋值决定作用域

只要在函数体内对某个变量赋值,Python 在编译时就将其视为局部变量,无论赋值语句出现在何处。

# 在外部进行了定义
# 但是变量 b 被视为局部变量,因为它在函数体内被赋值
b = 6
def f2(a):
    print(a)
    print(b)  # ← 报错
    # 
    b = 9

虽然 print(b)b = 9 之前,但因函数内存在对 b 的赋值,整个函数体中 b 都被视为局部变量。调用 f2(3) 时,print(a) 成功(输出 3),但 print(b) 尝试读取尚未赋值的局部变量 b,导致报错。

UnboundLocalError: local variable 'b' referenced before assignment

这不是 bug,而是 Python 的设计:无需显式声明变量,但赋值即定义为局部。相比 JavaScript(未声明变量会污染全局),此行为更安全。

显式声明全局变量global

若需在函数内修改全局变量,必须使用 global 声明:

b = 6
def f3(a):
    # 显示声明
    global b
    print(a)
    print(b)
    b = 9
# 输出 3 6 
# 之后全局变量 b 变为9
  1. 局部作用域(Local)
    函数参数及函数体内赋值的变量。
  2. 全局作用域(Global / Module-level)
    模块顶层定义的名称。
  3. 非局部作用域(Nonlocal)
    由嵌套函数中的闭包引入。

作用域判定

通过反汇编可验证作用域判定发生在编译阶段

# f1的字节码 此时b为全局
dis(f1)
# ...
LOAD_GLOBAL 1 (b)  # ← 从全局加载 b
# f2的字节吗 此时b被视为局部
dis(f2)
# ...
LOAD_FAST 1 (b)    # ← 试图从局部加载 b(但尚未赋值)
STORE_FAST 1 (b)   # 赋值发生在之后

LOAD_FAST 表示访问局部变量;LOAD_GLOBAL 表示访问全局变量;编译器在函数定义时就已根据是否存在赋值决定使用哪种指令。此机制说明:变量作用域由静态分析(编译时)决定,而非运行时动态查找。

闭包

闭包是一个函数(记为 f),它拥有一个扩展的作用域,该作用域包含在 f 的函数体中引用、但既非全局变量也非 f 的局部变量的变量。这些变量必须来自包含 f 的外层函数的局部作用域。函数是否匿名(如 lambda)无关紧要;关键在于能否访问其外部非全局变量。**闭包 ≠ 匿名函数。**混淆源于两者常在嵌套函数场景中同时出现。

实现方式

# 面向对象表示
class Averager():
    def __init__(self):
        # 历史数据保存在实例属性 self.series 中。
        self.series = []

    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total / len(self.series)

# 使用方式
avg = Averager()
print(avg(10))  # 10.0
print(avg(11))  # 10.5
print(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

# 使用方式
avg = make_averager()
print(avg(10))  # 10.0
print(avg(11))  # 10.5
print(avg(15))  # 12.0

averager 是一个闭包,它“记住”了外层函数 make_averager 中的 series 列表。即使 make_averager() 已返回、其局部作用域已销毁,series 仍可通过闭包访问。

自由变量

averager 中,series 是一个自由变量(free variable),它在 averager 的局部作用域中未被绑定(即未赋值),但被引用。

自由变量的绑定

Python 通过以下机制保存自由变量的绑定:

# 函数对象的元数据
print(avg.__code__.co_varnames)   # ('new_value', 'total')
print(avg.__code__.co_freevars)   # ('series',)
  • co_varnames:局部变量名;
  • co_freevars:自由变量名。
#  闭包的实际存储 __closure__
print(avg.__closure__)                    # (<cell at 0x...: list object at 0x...>,)
print(avg.__closure__[0].cell_contents)   # [10, 11, 15]

__closure__ 是一个元组,每个元素对应一个自由变量;每个元素是 cell 对象,其 cell_contents 属性保存变量的实际值。

总的来说,闭包保留了定义时自由变量的绑定,即使外层函数已退出。必要条件是存在嵌套函数;内层函数引用了外层函数的局部变量;外层函数返回内层函数(或以其他方式使其在外部可调用)。闭包使得函数式编程中实现状态保持(如累加器、计数器、缓存等)成为可能,而无需依赖类或全局变量。

nonlocal 声明

背景

在闭包中,若尝试对不可变类型(如 intstrtuple)的自由变量进行赋值(如 count += 1),会触发 UnboundLocalError

# 有缺陷的实现
def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        count += 1          # 等价于 count = count + 1
        total += new_value  # 同样是重新赋值
        return total / count
    return averager

[!NOTE]

Python 在编译时判定 counttotalaverager局部变量

只要在函数体内对某个变量进行了赋值(无论位置在何处),该变量在整个函数作用域中都被视为局部变量。

count += 1 实际是 count = count + 1,属于赋值操作;Python 在编译时判定 counttotalaverager局部变量;但调用时局部变量尚未初始化,导致:

UnboundLocalError: local variable 'count' referenced before assignment
# 函数式实现
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(10))  # 10.0
print(avg(11))  # 10.5
print(avg(15))  # 12.0

对比:上述代码中使用 series.append()就地修改可变对象,是调用方法修改可变对象的内容,并未对 series 这个名字进行重新绑定,因此 series 仍是自由变量,可被闭包捕获。

[!IMPORTANT]

可变对象(mutable objects)

  • 特点:对象创建后,其内容可以被修改,而对象的身份(id)保持不变
  • 修改方式:通过调用对象的原地修改方法(如 list.append()dict.update()set.add() 等)。
  • 关键点:这些操作不改变变量名的绑定,只是修改了对象内部状态。
lst = [1, 2]
print(id(lst))      # 例如 140234...
lst.append(3)       # 修改内容,未重新赋值
print(id(lst))      # 仍是 140234...(同一对象)

不可变对象(immutable objects)

  • 特点:对象一旦创建,其内容不能被修改
  • “修改”方式:只能通过创建新对象,并将变量名重新绑定到新对象。
  • 关键点:任何看似“修改”的操作(如 x += 1s = s + "a")实际上都是创建新对象 + 重新赋值
x = 10
print(id(x))        # 例如 945...
x += 1              # 等价于 x = x + 1 → 创建新 int 对象
print(id(x))        # 变为另一个值(新对象)

解决方案

Python 3 引入 nonlocal 关键字,用于显式声明变量来自外层函数作用域,即使在内层函数中对其赋值。

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        nonlocal count, total  # 声明为非局部变量
        count += 1
        total += new_value
        return total / count
    return averager

counttotal 被视为外层函数 make_averager 的局部变量;赋值操作会更新闭包中存储的绑定;使代码可正常工作。

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

Python 变量查找逻辑

当函数中引用变量 x 时,Python 按以下规则确定其作用域:

  1. global x 声明
    x 来自并赋值于模块全局作用域

  2. nonlocal x 声明
    x 来自并赋值于最近的外层函数局部作用域(即闭包中的自由变量)。

  3. x 是参数或在函数体内被赋值
    x局部变量

  4. x 仅被引用(未赋值、非参数)
    查找顺序为:

    • 外层函数的局部作用域(逐层向外,即非局部作用域);
    • 模块全局作用域;
    • 内置命名空间(__builtins__.__dict__)。

注意:Python 没有“程序级”全局作用域,只有模块级全局作用域

实现简单装饰器

简易版本

import time

def clock(func):
    # *args接受任意位置参数 但是无法处理关键字参数
    def clocked(*args):                      
        # 记录函数调用开始时间(高精度计时器)
        t0 = time.perf_counter()
        # 通过闭包访问自由变量 func
        result = func(*args)                 
        # 计算函数执行耗时(结束时间 - 开始时间)
        elapsed = time.perf_counter() - t0
        # 获取原始函数的名称
        name = func.__name__
        # 将所有位置参数转换为可打印的字符串表示
        arg_str = ', '.join(repr(arg) for arg in args)
        # 打印格式化的调用日志:
        # [耗时] 函数名(参数) -> 返回值
        # 示例:[0.12363791s] snooze(0.123) -> None
        print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
        # 返回原始函数的计算结果,确保装饰后的函数行为与原函数一致
        return result
    # 返回内部函数,替换原函数
    # 导致内部函数完全取代原函数 从而导致原函数性质丢失
    return clocked                          
import time
from clockdeco0 import clock

# @clock 语法等价于 factorial = clock(factorial)
@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.12363791s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000095s] factorial(1) -> 1
[0.00002408s] factorial(2) -> 2
...
[0.00008297s] factorial(6) -> 720
6! = 720

保留元数据

基础版本不支持关键字参数(**kwargs);覆盖了被装饰函数的 __name____doc__ 等元数据。

import time
import functools

def clock(func):
    @functools.wraps(func)  # 保留原函数元数据
    def clocked(*args, **kwargs):  # 支持关键字参数
        t0 = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_lst = [repr(arg) for arg in args]
        arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items())
        arg_str = ', '.join(arg_lst)
        print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
        return result
    return clocked

@functools.wraps(func)func__name____doc____module__ 等属性复制到 clocked,避免元数据丢失。*args, **kwargs:使装饰器能处理任意位置参数和关键字参数。

装饰器的本质

装饰器动态地为函数附加额外职责(如日志、计时、缓存等);实现方式是用新函数替换原函数,新函数接收相同参数;执行额外逻辑;返回原函数应有的结果。

标准库中的装饰器

使用 functools.cache 实现记忆化

记忆化(memoization)即缓存函数调用结果,避免对相同参数重复计算。functools.cache要求所有参数必须可哈希(因为缓存键基于参数元组)。

# 未缓版本斐波那契数列
from clockdeco import clock

@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)
# fibonacci(30)需要调用2,692,537次

[!NOTE]

print(fibonacci(6))

[0.00000042s] fibonacci(0) -> 0
[0.00000049s] fibonacci(1) -> 1
[0.00006115s] fibonacci(2) -> 1
[0.00000031s] fibonacci(1) -> 1
...
[0.00016852s] fibonacci(6) -> 8
8
# fibonacci(1) 被调用了 8 次 # fibonacci(2) 调用了 5 次
# 缓存版本
import functools
from clockdeco import clock

@functools.cache  
@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)
# fibonacci(30) 仅调用31 次
# 使用升级版本
$ python3 fibo_demo_lru.py
[0.00000043s] fibonacci(0) -> 0
[0.00000054s] fibonacci(1) -> 1
[0.00006179s] fibonacci(2) -> 1
[0.00000070s] fibonacci(3) -> 2
[0.00007366s] fibonacci(4) -> 3
[0.00000057s] fibonacci(5) -> 5
[0.00008479s] fibonacci(6) -> 8
8

堆叠装饰器顺序:从下到上应用
@cache + @clock 等价于 fibonacci = cache(clock(fibonacci))
→ 先包装为 clocked,再对 clocked 应用缓存。

但是要注意@cache 无大小限制,可能耗尽内存。其适合短生命周期脚本;长期运行服务应使用 @lru_cache

使用 functools.lru_cache

@functools.cache 实际是 @lru_cache(maxsize=None) 的别名。@lru_cache 支持maxsize:缓存最大条目数(默认 128,建议设为 2 的幂);typed:若为 True,则 11.0 被视为不同键(默认为False)。

# Python 3.8+
@lru_cache
def f(x): ...

# Python 3.2+
@lru_cache()
def f(x): ...

# 自定义参数
@lru_cache(maxsize=2**20, typed=True)
def f(x): ...

单分派泛型函数@singledispatch

Python 无方法重载机制。传统方案(if/elifmatch/case不可扩展,且难以维护。@singledispatch 允许模块化注册专用实现,支持第三方类型。

核心机制在于被 @singledispatch 装饰的函数成为泛型函数入口。运行时根据第一个参数的类型动态选择实现(单分派)。基础函数自动注册为 object 类型(兜底实现)。

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


# 使用 @singledispatch 将 htmlize 定义为单分派泛型函数的默认实现
# 此函数处理所有未被其他专用函数覆盖的类型(兜底实现)
@singledispatch
def htmlize(obj: object) -> str:
    """默认实现:对任意对象调用 repr(),进行 HTML 转义后包裹在 <pre> 标签中"""
    content = html.escape(repr(obj))  # 转义特殊字符(如 <, >, &, " 等)
    return f'<pre>{content}</pre>'


# 为 str 类型注册专用实现
@htmlize.register
def _(text: str) -> str:
    """处理字符串:转义后将换行符 \n 替换为 <br/>\n,并用 <p> 标签包裹"""
    # 注意:html.escape 会转义 &、<、> 等,确保内容安全
    content = html.escape(text).replace('\n', '<br/>\n')
    return f'<p>{content}</p>'


# 为 abc.Sequence(如 list、tuple)注册专用实现
# 注意:str 也是 Sequence,但由于 str 的注册更具体,会优先匹配 str 实现
@htmlize.register
def _(seq: abc.Sequence) -> str:
    """处理序列类型(如 list、tuple):递归格式化每个元素,生成 HTML 无序列表"""
    # 对序列中每个元素递归调用 htmlize,获得其 HTML 表示
    inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
    return '<ul>\n<li>' + inner + '</li>\n</ul>'


# 为 numbers.Integral(如 int、numpy 整数等)注册专用实现
@htmlize.register
def _(n: numbers.Integral) -> str:
    """处理整数类型:显示十进制值和对应的十六进制形式(如 42 → 42 (0x2a))"""
    return f'<pre>{n} (0x{n:x})</pre>'


# 为 bool 类型注册专用实现
# 注意:bool 是 int 的子类,也是 numbers.Integral 的子类,
# 但 singledispatch 会优先选择最具体的类型(bool),因此此实现生效
@htmlize.register
def _(n: bool) -> str:
    """处理布尔值:仅显示 True/False,不显示十六进制(与 int 区分)"""
    return f'<pre>{n}</pre>'


# 显式为 fractions.Fraction 类型注册专用实现(无类型注解,直接传入类型)
@htmlize.register(fractions.Fraction)
def _(x) -> str:
    """处理 Fraction 对象:直接显示为“分子/分母”形式(如 Fraction(2, 3) → 2/3)"""
    frac = fractions.Fraction(x)  # 确保输入为 Fraction(防御性转换)
    return f'<pre>{frac.numerator}/{frac.denominator}</pre>'


# 同一实现同时注册给 decimal.Decimal 和 float 类型
@htmlize.register(decimal.Decimal)
@htmlize.register(float)
def _(x) -> str:
    """处理浮点数和 Decimal:显示原始值,并附加一个近似分数表示"""
    # 使用 Fraction(x).limit_denominator() 获取最接近的简单分数
    frac = fractions.Fraction(x).limit_denominator()
    return f'<pre>{x} ({frac.numerator}/{frac.denominator})</pre>'

带参数的装饰器

当装饰器需要接收配置参数(如 @lru_cache(maxsize=128))时,不能直接将参数传给装饰器函数。
解决方法是:使用“装饰器工厂”模式,一个返回真正装饰器的函数。

装饰器工厂 ≠ 装饰器。
@decorator(...) 的执行顺序是:先调用 decorator(...) 得到装饰器,再用该装饰器装饰函数。

基本结构

带参数装饰器通常包含三层函数嵌套:

  1. 最外层(装饰器工厂)
    接收用户传入的配置参数(如 active=True, fmt=...)。
  2. 中间层(真正的装饰器)
    接收被装饰的函数 func
  3. 最内层(包装函数,可选)
    替换原函数,添加额外逻辑(如计时、缓存、日志),并返回原函数结果。

若装饰器仅做副作用(如注册),可省略包装层,直接返回 func

示例

registry = set()
# 装饰器工厂
# 根据配置参数 active,动态决定是否将被装饰的函数注册(加入或移除)到一个全局注册表(registry)中。 
def register(active=True):
    # 真正的装饰器
    def decorate(func):             
        print(f'running register(active={active})->decorate({func})')
        if active:
            registry.add(func)
        else:
            # 安全移除
            registry.discard(func)  
        # 直接返回原函数
        return func        
    # 返回装饰器
    return decorate                
@register(active=False)
def f1(): pass

@register()  # 等价于 @register(active=True)
def f2(): pass

必须写 @register()(带括号),否则 @register 会把函数对象 register 当作装饰器,导致类型错误。

同时支持运行时动态注册:

register()(f3)           # 注册 f3
# # 等价于 @register() def f3(): ...
register(active=False)(f2)  # 取消注册 f2

示例 2 带参数的装饰器

import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'

# 装饰器工厂 接受格式字符串
def clock(fmt=DEFAULT_FMT):
    # 装饰器:接收函数
    def decorate(func):
        # 包装函数:拦截调用
        def clocked(*_args):   
            t0 = time.perf_counter()
            _result = func(*_args)
            elapsed = time.perf_counter() - 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
@clock()  # 默认格式
def snooze(seconds): time.sleep(seconds)

@clock('{name}: {elapsed:.3f}s')  # 自定义格式
def work(n): ...

利用 **locals() 将局部变量(elapsed, name, args, result)注入格式化上下文,极大提升灵活性。虽然静态分析工具可能警告“变量未使用”,但这是 Python 动态特性的合理应用。

[!NOTE]

clocked 函数体内,以下局部变量已被定义:

t0 = time.perf_counter()
_result = func(*_args)
elapsed = time.perf_counter() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args)
result = repr(_result)

因此,调用 locals() 时,会得到一个类似这样的字典(实际值取决于运行时):

{
    't0': 123456.789,
    '_result': None,
    'elapsed': 0.12345678,
    'name': 'snooze',
    'args': '0.123',
    'result': 'None',
    '_args': (0.123,),
    'func': <function snooze at 0x...>,
    'fmt': '[{elapsed:0.8f}s] {name}({args}) -> {result}'
}

注意:fmt 之所以也在 locals() 中,是因为它是从外层作用域(decorate闭包捕获的变量,在 clocked 中可读,因此也被包含在 locals() 返回结果中(CPython 实现中,闭包变量在 locals() 中可见)。

因为 fmt 是一个格式字符串,例如:

'[{elapsed:0.8f}s] {name}({args}) -> {result}'

当执行:

fmt.format(**locals())

等价于:

fmt.format(
    elapsed=0.12345678,
    name='snooze',
    args='0.123',
    result='None',
    # ... 其他变量也传入,但 format 只用到需要的
)

Python 的 str.format() 会自动从关键字参数中查找 {elapsed}{name} 等占位符对应的值。

即使 locals() 包含了多余变量(如 t0, _result),format() 也会忽略未使用的键,只要所需字段都存在就不会报错。 所以,**locals() 能成功提供 fmt.format() 所需的所有命名字段。

示例 3 基于类的实现

import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'

class clock:  # 小写类名:强调其作为函数式装饰器的替代
    def __init__(self, fmt=DEFAULT_FMT):
        self.fmt = fmt               # 保存配置

    def __call__(self, func):        # 使实例可调用,充当装饰器
        def clocked(*_args):
            t0 = time.perf_counter()
            _result = func(*_args)
            elapsed = time.perf_counter() - t0
            name = func.__name__
            args = ', '.join(repr(arg) for arg in _args)
            result = repr(_result)
            print(self.fmt.format(**locals()))
            return _result
        return clocked

当前实现的优点在于状态管理清晰,配置参数(fmt)作为实例属性存储。同时易于扩展,可轻松添加 __wrapped__cache_clear() 等接口。符合 Python 习惯,许多标准库装饰器(如 functools.lru_cache)内部也采用类实现。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值