当前位置: 首页 > news >正文

第九章 装饰器与闭包

第九章 装饰器与闭包

定义装饰器

装饰器是一个可调用对象(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 bprint(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 = 0total = 0def averager(new_value):count += 1          # 等价于 count = count + 1total += new_value  # 同样是重新赋值return total / countreturn 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 = 0total = 0def averager(new_value):nonlocal count, total  # 声明为非局部变量count += 1total += new_valuereturn total / countreturn 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 timedef clock(func):# *args接受任意位置参数 但是无法处理关键字参数def clocked(*args):                      # 记录函数调用开始时间(高精度计时器)t0 = time.perf_counter()# 通过闭包访问自由变量 funcresult = func(*args)                 # 计算函数执行耗时(结束时间 - 开始时间)elapsed = time.perf_counter() - t0# 获取原始函数的名称name = func.__name__# 将所有位置参数转换为可打印的字符串表示arg_str = ', '.join(repr(arg) for arg in args)# 打印格式化的调用日志:# [耗时] 函数名(参数) -> 返回值# 示例:[0.12363791s] snooze(0.123) -> Noneprint(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 functoolsdef clock(func):@functools.wraps(func)  # 保留原函数元数据def clocked(*args, **kwargs):  # 支持关键字参数t0 = time.perf_counter()result = func(*args, **kwargs)elapsed = time.perf_counter() - t0name = 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 resultreturn 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 nreturn 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 nreturn 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 timeDEFAULT_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() - t0name = func.__name__args = ', '.join(repr(arg) for arg in _args)result = repr(_result)# 动态格式化print(fmt.format(**locals()))return _resultreturn clockedreturn 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 timeDEFAULT_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() - t0name = func.__name__args = ', '.join(repr(arg) for arg in _args)result = repr(_result)print(self.fmt.format(**locals()))return _resultreturn clocked

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

http://www.dtcms.com/a/482452.html

相关文章:

  • 接口测试案例从哪些维度去设计
  • 协程入门(基础篇)
  • 建设好网站的在线沟通功能广州开发区投资集团有限公司招聘
  • 如何将 iPhone 联系人同步到 Mac
  • 织梦的网站收录不好保定网站建设设计
  • 网络安全之揭秘APT Discord C2 以及如何取证
  • 第五章 神经网络的优化
  • 网络安全主动防御技术与应用
  • 5. 神经网络的学习
  • 响应式网站页面设计怎么写网站建设推广
  • 2025/10/14 redis断联 没有IPv4地址 (自用)
  • 基于多奥品牌设备的车牌识别系统与电梯门禁联动方案,核心是通过硬件信号对接+软件权限映射实现车辆身份与电梯权限的绑定。以下是具体实施步骤:
  • [Backstage] 前端插件 生命周期 | eg构建“云成本”页面
  • extractNativeLibs属性解刨
  • 实现一个通用的 `clone` 函数:从深拷贝到类型安全的 C++ 模板设计
  • dw做网站基础用友财务软件多少钱一年
  • 高端定制网站建设制作网页制作格式
  • java + vue 实现 AI流式输出(打字机效果)
  • Linux网络:使用TCP实现网络通信(服务端)
  • Python Web开发——WSGI接口
  • 第十章:技术路线:成为“技术扫地僧(1)
  • 苹果软件混淆与 iOS 应用加固实录,从被逆向到 IPA 文件防反编译与无源码混淆解决方案
  • Transformers中从 logits 本质到问答系统中的字符定位机制
  • c++11扩展
  • h1z1注册网站百度app官方下载
  • 阮一峰《TypeScript 教程》学习笔记——基本用法
  • LabVIEW腔衰荡信号在线处理系统
  • 为 AI Agent 行为立“规矩”——字节跳动提出 Jeddak AgentArmor 智能体安全框架
  • Arbess CICD实战(12) - 使用Arbess+GitLab实现React.js项目自动化部署
  • 网站如何做延迟加载店铺图片免费生成