【Python基础篇-09】深入理解python的闭包与装饰器

深入理解python的闭包与装饰器

  • 一、闭包与装饰器
    • [1. 先回顾:函数也是对象](#1. 先回顾:函数也是对象)
      • [1.1 函数可以赋给变量](#1.1 函数可以赋给变量)
      • [1.2 函数可以作为参数](#1.2 函数可以作为参数)
      • [1.3 函数可以作为返回值](#1.3 函数可以作为返回值)
    • [2. 闭包:函数保留对外层变量的访问](#2. 闭包:函数保留对外层变量的访问)
      • [2.1 从一个带配置的函数开始](#2.1 从一个带配置的函数开始)
      • [2.2 闭包保留的不是整个外层函数](#2.2 闭包保留的不是整个外层函数)
      • [2.3 不同调用可以创建独立的闭包](#2.3 不同调用可以创建独立的闭包)
    • [3. 闭包中的状态与 nonlocal](#3. 闭包中的状态与 nonlocal)
      • [3.1 创建一个计数器](#3.1 创建一个计数器)
      • [3.2 为什么需要 nonlocal](#3.2 为什么需要 nonlocal)
      • [3.3 修改可变对象不一定需要 nonlocal](#3.3 修改可变对象不一定需要 nonlocal)
      • [3.4 不同计数器的状态独立](#3.4 不同计数器的状态独立)
    • [4. 闭包的延迟绑定](#4. 闭包的延迟绑定)
      • [4.1 为什么循环创建的函数都返回最后一个值](#4.1 为什么循环创建的函数都返回最后一个值)
      • [4.2 使用默认参数绑定当前值](#4.2 使用默认参数绑定当前值)
      • [4.3 使用工厂函数创建独立绑定](#4.3 使用工厂函数创建独立绑定)
    • [5. 从手动包装到装饰器](#5. 从手动包装到装饰器)
      • [5.1 一个重复出现的需求](#5.1 一个重复出现的需求)
      • [5.2 手动包装函数](#5.2 手动包装函数)
      • [5.3 替换原来的名称](#5.3 替换原来的名称)
      • [5.4 使用 @ 语法](#5.4 使用 @ 语法)
    • [6. 编写通用函数装饰器](#6. 编写通用函数装饰器)
      • [6.1 转发参数和返回值](#6.1 转发参数和返回值)
      • [6.2 使用 functools.wraps 保留元信息](#6.2 使用 functools.wraps 保留元信息)
      • [6.3 异常时是否仍需执行收尾逻辑](#6.3 异常时是否仍需执行收尾逻辑)
    • [7. 带参数的装饰器](#7. 带参数的装饰器)
      • [7.1 为什么需要再加一层函数](#7.1 为什么需要再加一层函数)
      • [7.2 完整示例](#7.2 完整示例)
      • [7.3 近似展开](#7.3 近似展开)
    • [8. 装饰时机与多个装饰器的顺序](#8. 装饰时机与多个装饰器的顺序)
      • [8.1 装饰发生在执行函数定义时](#8.1 装饰发生在执行函数定义时)
      • [8.2 多个装饰器由下向上应用](#8.2 多个装饰器由下向上应用)
      • [8.3 调用时从外层进入](#8.3 调用时从外层进入)
      • [8.4 顺序会改变实际语义](#8.4 顺序会改变实际语义)
    • [9. 使用 lru_cache 缓存结果](#9. 使用 lru_cache 缓存结果)
      • [9.1 为可复用计算增加缓存](#9.1 为可复用计算增加缓存)
      • [9.2 参数必须可哈希](#9.2 参数必须可哈希)
      • [9.3 清理缓存](#9.3 清理缓存)
      • [9.4 不适合缓存哪些函数](#9.4 不适合缓存哪些函数)
      • [9.5 缓存实例方法也有边界](#9.5 缓存实例方法也有边界)
    • [10. 装饰实例方法](#10. 装饰实例方法)
      • [10.1 普通包装函数通常可以接收 self](#10.1 普通包装函数通常可以接收 self)
      • [10.2 与 classmethod、staticmethod 组合](#10.2 与 classmethod、staticmethod 组合)

如需转载,请附上链接:https://zhenghuisheng.blog.csdn.net/article/details/166900653

一、闭包与装饰器

前面我们已经学习了函数,并知道函数可以赋给变量、作为参数传入,也可以作为另一个函数的返回值。在面向对象章节中,还见过:

复制代码
@property
@classmethod
@staticmethod

这些带 @ 的写法就是装饰器的应用。装饰器主要用来组织一些重复出现的行为。例如,多个函数都需要:

  • 记录执行日志。
  • 统计运行时间。
  • 检查参数。
  • 缓存计算结果。

如果把这些代码分别写进每个函数,就容易重复,也不方便统一修改。本篇从闭包开始,逐步理解装饰器的工作方式,并学习如何实现带参数的装饰器、叠加多个装饰器,以及使用标准库中的缓存装饰器。

1. 先回顾:函数也是对象

1.1 函数可以赋给变量

python 复制代码
def greet(name):
    return f"你好,{name}"
    
if __name__ == '__main__':
    operation = greet
    print(operation("小林"))
    print(operation is greet)

运行结果:

复制代码
你好,小林
True

这里:

复制代码
operation = greet

没有执行函数,只是让 operation 指向同一个函数对象。

复制代码
result = greet("小林")

1.2 函数可以作为参数

python 复制代码
def square(number):
    return number ** 2
def calculate(number, operation):
    return operation(number)

if __name__ == '__main__':
    print(calculate(5, square))

运行结果:

复制代码
25

calculate() 接收一个函数,并在内部调用它。装饰器同样会接收函数,只是它通常还会返回一个新的可调用对象。

1.3 函数可以作为返回值

python 复制代码
def create_greeting():
    def greet(name):
        return f"你好,{name}"
    return greet

if __name__ == '__main__':
    greeting = create_greeting()
    print(greeting("小林"))

运行结果:

复制代码
你好,小林

需要注意:

复制代码
return greet

返回的是函数对象。

复制代码
return greet(...)

2. 闭包:函数保留对外层变量的访问

2.1 从一个带配置的函数开始

假设需要创建不同折扣的计算函数:

python 复制代码
def make_discount(rate):
    def calculate(amount):
        return amount * rate
    return calculate

if __name__ == '__main__':
    discount_80 = make_discount(0.8)
    discount_90 = make_discount(0.9)
    print(discount_80(100))
    print(discount_90(100))

运行结果:

复制代码
80.0
90.0

make_discount() 已经返回,但内部函数 calculate() 仍然能够访问外层的 rate。这种函数与它所引用的外层变量绑定共同存在的结构,称为闭包 。这里的金额只是用于说明函数行为。实际金额计算仍应根据业务选择整数分或 Decimal。

2.2 闭包保留的不是整个外层函数

在上面的例子中:

python 复制代码
def calculate(amount):
    return amount * rate

rate 既不是 calculate() 的参数,也不是它内部定义的局部变量,而是来自外层函数。闭包让这个被引用的外层绑定在需要时继续存在,并不是把外层函数的所有局部变量都无条件保存下来。

也不是所有嵌套函数都会形成闭包。如果内部函数没有引用外层局部变量,就不需要通过闭包保存这些绑定。

2.3 不同调用可以创建独立的闭包

python 复制代码
def make_prefixer(prefix):
    def add_prefix(text):
        return f"{prefix}{text}"
    return add_prefix


if __name__ == '__main__':
    warning = make_prefixer("[警告] ")
    info = make_prefixer("[信息] ")

    print(warning("库存不足"))
    print(info("保存完成"))

运行结果:

复制代码
[警告] 库存不足
[信息] 保存完成

两次调用 make_prefixer(),分别建立自己的外层绑定,因此可以得到带不同配置的函数。闭包适合这种需求:创建一次配置,之后重复执行同一种操作。

一句话判断就是:这个内部函数里的变量,是不是必须去它的外层函数作用域里找?

3. 闭包中的状态与 nonlocal

3.1 创建一个计数器

python 复制代码
def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment


if __name__ == '__main__':
    counter = make_counter()
    print(counter())
    print(counter())
    print(counter())

运行结果:

复制代码
1
2
3

count 保存在闭包中,每次调用 increment() 都会继续更新它。

3.2 为什么需要 nonlocal

复制代码
count += 1

会对名称 count 重新赋值。如果没有 nonlocal,Python 会把它视为内部函数的局部名称;但计算右侧时,这个局部名称还没有值,就会产生 UnboundLocalError。

python 复制代码
nonlocal count

表示使用并重新绑定外层函数中已经存在的 count。

3.3 修改可变对象不一定需要 nonlocal

python 复制代码
def make_recorder():
    records = []
    def record(message):
        records.append(message)
        return records.copy()
    return record

if __name__ == '__main__':
    recorder = make_recorder()
    print(recorder("开始"))
    print(recorder("结束"))

运行结果:

复制代码
['开始']
['开始', '结束']

这里没有重新绑定名称 records,只是修改它引用的列表,因此不需要 nonlocal。

与前面参数和作用域章节的规则一致:

  • records.append(...):修改对象。
  • records = [...]:重新绑定名称。

3.4 不同计数器的状态独立

python 复制代码
def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

if __name__ == '__main__':
    first = make_counter()
    second = make_counter()
    print(first())
    print(first())
    print(second())

运行结果:

复制代码
1
2
1

但如果只是:

复制代码
second = first

两个变量就会引用同一个闭包函数,共享同一份计数状态。

4. 闭包的延迟绑定

4.1 为什么循环创建的函数都返回最后一个值

python 复制代码
def build_functions():
    functions = []
    for number in range(3):
        def get_number():
            return number
        functions.append(get_number)
    return functions

if __name__ == '__main__':
    functions = build_functions()
    print([function() for function in functions])

运行结果:

复制代码
[2, 2, 2]

这些函数引用的是同一个外层变量绑定 number。等到真正调用时,循环已经结束,number 的值为 2。闭包通常保留对变量绑定的访问,而不是自动保存定义当时的值快照。

4.2 使用默认参数绑定当前值

python 复制代码
def build_functions():
    functions = []
    for number in range(3):
        def get_number(value=number):
            return value
        functions.append(get_number)
    return functions

if __name__ == '__main__':
    print([function() for function in build_functions()])

运行结果:

复制代码
[0, 1, 2]

默认参数表达式在执行函数定义时求值,因此每次记录当前的 number。这里使用的是默认参数机制,不是让闭包突然改变了绑定规则。

4.3 使用工厂函数创建独立绑定

另一种方式是每次调用工厂函数:

python 复制代码
def make_getter(value):
    def get_value():
        return value
    return get_value

def build_functions():
    return [make_getter(number) for number in range(3)]

if __name__ == '__main__':
    print([function() for function in build_functions()])

运行结果:

复制代码
[0, 1, 2]

每次调用 make_getter() 都创建独立的 value 绑定,因此不会共享循环变量。

5. 从手动包装到装饰器

5.1 一个重复出现的需求

假设多个函数都需要在执行前后记录日志:

python 复制代码
def calculate_total(price, quantity):
    print("开始执行")
    result = price * quantity
    print("执行结束")
    return result

如果每个业务函数都加入相同日志,就会产生重复代码。可以把公共行为移到一个包装函数中。

5.2 手动包装函数

python 复制代码
def add_log(func):
    def wrapper():
        print("开始执行")
        result = func()
        print("执行结束")
        return result
    return wrapper

def greet():
    print("你好,Python")

if __name__ == '__main__':
    decorated_greet = add_log(greet)
    decorated_greet()

运行结果:

python 复制代码
开始执行
你好,Python
执行结束

这里的关系是:

  1. add_log() 接收原函数 greet。
  2. 创建内部函数 wrapper。
  3. wrapper 通过闭包保留对原函数 func 的访问。
  4. 返回 wrapper。
  5. 调用 decorated_greet(),实际执行包装函数。

5.3 替换原来的名称

可以直接写:

复制代码
greet = add_log(greet)

之后:

复制代码
greet()

调用的就是包装后的函数。这不会原地修改原函数的代码,而是让名称 greet 重新绑定到包装后的函数。

5.4 使用 @ 语法

下面的写法:

python 复制代码
@add_log
def greet():
    print("你好,Python")

可以近似理解为:

python 复制代码
def greet():
    print("你好,Python")
greet = add_log(greet)

完整示例:

python 复制代码
def add_log(func):
    def wrapper():
        print("开始执行")
        result = func()
        print("执行结束")
        return result
    return wrapper

@add_log
def greet():
    print("你好,Python")

if __name__ == '__main__':
    greet()

运行结果:

复制代码
开始执行
你好,Python
执行结束

这样的 add_log 就是一个函数装饰器。

更一般地说,装饰器是接收被装饰对象并返回替代对象的可调用对象。本文主要关注接收函数并返回包装函数的常见形式。

6. 编写通用函数装饰器

6.1 转发参数和返回值

前面的 wrapper() 不接收参数,只适合无参函数。可以使用 *args 和 **kwargs:

python 复制代码
def add_log(func):
    def wrapper(*args, **kwargs):
        print(f"开始执行:{func.__name__}")
        result = func(*args, **kwargs)
        print(f"执行结束:{func.__name__}")
        return result

    return wrapper

@add_log
def calculate_total(price, quantity=1):
    return price * quantity

if __name__ == '__main__':
    total = calculate_total(8, quantity=3)
    print(f"金额:{total}")

运行结果:

复制代码
开始执行:calculate_total
执行结束:calculate_total
金额:24

这里的关键是:

复制代码
func(*args, **kwargs)

将包装函数收到的位置实参和关键字实参交给原函数。以及:

复制代码
return result

将原函数的返回结果继续交给调用方。如果遗漏 return,调用方得到的就会是 None。

6.2 使用 functools.wraps 保留元信息

包装后,外部名称实际上指向 wrapper:

复制代码
print(calculate_total.__name__)

如果没有额外处理,会得到:

复制代码
wrapper

原来的函数名称、文档字符串等信息也可能被遮住。可以使用标准库的 wraps:

python 复制代码
from functools import wraps

def add_log(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"开始执行:{func.__name__}")
        result = func(*args, **kwargs)
        print(f"执行结束:{func.__name__}")
        return result
    return wrapper

@add_log
def calculate_total(price, quantity=1):
    """根据单价和数量计算金额。"""
    return price * quantity

if __name__ == '__main__':
    print(calculate_total.__name__)
    print(calculate_total.__doc__)
    print(calculate_total(8, 3))

运行结果:

复制代码
calculate_total
根据单价和数量计算金额。
开始执行:calculate_total
执行结束:calculate_total
24

wraps() 会更新包装函数的相关元信息,并设置 __wrapped__ 指向被包装函数,方便检查工具追踪原函数。它不会自动修复错误的参数转发、返回值或业务行为。

6.3 异常时是否仍需执行收尾逻辑

下面的写法:

复制代码
result = func(*args, **kwargs)
print("执行结束")

如果原函数抛出异常,第二行不会执行。

如果希望无论成功失败都记录耗时,可以使用 finally:

python 复制代码
​```python
# 从 functools 模块中导入 wraps
# wraps 主要用于装饰器中:
# 它可以让被装饰后的函数保留原函数的名字、文档等元信息
from functools import wraps

# 从 time 模块中导入 perf_counter
# perf_counter() 是一个高精度计时器,
# 很适合用来统计一段代码的执行耗时
from time import perf_counter


# 定义一个装饰器函数 timed
# func 表示将来被装饰的原函数
def timed(func):

    # wraps(func) 表示:
    # wrapper 虽然替代了原函数执行,
    # 但仍然保留原函数 func 的函数名、文档等信息
    @wraps(func)
    def wrapper(*args, **kwargs):

        # 记录函数开始执行时的时间
        started = perf_counter()

        try:
            # 调用原来的函数 func
            #
            # *args:
            # 接收任意数量的位置参数
            #
            # **kwargs:
            # 接收任意数量的关键字参数
            #
            # return 会把原函数的执行结果继续返回出去
            return func(*args, **kwargs)

        finally:
            # finally 中的代码无论函数是否正常执行完成,
            # 甚至发生异常,都会执行

            # 再次获取当前时间,并减去开始时间
            # 得到函数实际执行耗时
            elapsed = perf_counter() - started

            # 打印原函数的名称和执行耗时
            #
            # func.__name__:
            # 获取原函数名称
            #
            # elapsed:.6f:
            # 保留 6 位小数
            print(f"{func.__name__} 耗时:{elapsed:.6f} 秒")

    # 返回内部函数 wrapper
    #
    # 这里形成了闭包:
    # wrapper 内部使用了外层 timed 函数的 func 变量
    return wrapper


# 使用 timed 装饰 calculate_sum
#
# 这段代码:
#
# @timed
# def calculate_sum(...):
#
# 本质上等价于:
#
# calculate_sum = timed(calculate_sum)
#
# 所以最终 calculate_sum 实际指向的是 wrapper 函数
@timed
def calculate_sum(limit):

    # range(limit):
    # 生成 0 到 limit - 1 的整数序列
    #
    # sum(...):
    # 对这些整数进行求和
    return sum(range(limit))


# 只有当前 Python 文件被直接运行时,
# 才会执行下面的代码
#
# 如果这个文件是被其他 Python 文件 import,
# 这里的代码不会自动执行
if __name__ == '__main__':

    # 调用 calculate_sum
    #
    # 由于 calculate_sum 被 @timed 装饰,
    # 实际调用流程是:
    #
    # calculate_sum(100_000)
    #         ↓
    # wrapper(100_000)
    #         ↓
    # 记录开始时间
    #         ↓
    # 调用原来的 calculate_sum(100_000)
    #         ↓
    # 得到计算结果
    #         ↓
    # finally 中统计耗时
    #         ↓
    # 返回原来的计算结果
    result = calculate_sum(100_000)

    # 打印最终求和结果
    print(result)
​```

输出类似:

复制代码
calculate_sum 耗时:0.001234 秒
4999950000

这个装饰器没有捕获并吞掉原函数异常。如果原函数失败,记录耗时后,异常仍会继续向外传播。

7. 带参数的装饰器

7.1 为什么需要再加一层函数

假设希望日志装饰器能够配置操作名称:

复制代码
@log_action("查询商品")
def find_product(...):
    ...

此时:

复制代码
log_action("查询商品")

需要先返回一个真正接收函数的装饰器。因此通常会有三层:

python 复制代码
def 配置函数(配置参数):
    def 装饰器(原函数):
        def 包装函数(*args, **kwargs):
            ...
        return 包装函数
    return 装饰器

7.2 完整示例

python 复制代码
from functools import wraps


def log_action(action):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"[{action}] 开始")
            result = func(*args, **kwargs)
            print(f"[{action}] 完成")
            return result

        return wrapper

    return decorator


@log_action("查询商品")
def find_product(products, product_id):
    for product in products:
        if product["id"] == product_id:
            return product

    return None


if __name__ == '__main__':
    products = [
        {"id": 1001, "name": "矿泉水"},
        {"id": 1002, "name": "可乐"}
    ]

    print(find_product(products, 1002))

运行结果:

复制代码
[查询商品] 开始
[查询商品] 完成
{'id': 1002, 'name': '可乐'}

三层职责:

层级 接收什么 返回什么
log_action(action) 装饰器配置 decorator
decorator(func) 被装饰函数 wrapper
wrapper(*args, **kwargs) 实际调用参数 业务调用结果

7.3 近似展开

复制代码
@log_action("查询商品")
def find_product(...):
    ...

可以近似理解为:

python 复制代码
def find_product(...):
    ...


decorator = log_action("查询商品")
find_product = decorator(find_product)

这样看就能分清:配置参数与业务参数是在不同阶段传入的。

8. 装饰时机与多个装饰器的顺序

8.1 装饰发生在执行函数定义时

python 复制代码
from functools import wraps


def mark(label):
    print(f"创建装饰器:{label}")

    def decorator(func):
        print(f"包装函数:{func.__name__}")

        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"调用包装函数:{label}")
            return func(*args, **kwargs)

        return wrapper

    return decorator


@mark("演示")
def greet():
    print("执行原函数")


if __name__ == '__main__':
    print("进入主程序")
    greet()
    greet()

运行结果:

复制代码
创建装饰器:演示
包装函数:greet
进入主程序
调用包装函数:演示
执行原函数
调用包装函数:演示
执行原函数

创建和应用装饰器的操作,在函数定义执行时发生。

如果函数定义位于模块顶层,这些操作通常会在模块导入期间发生。因此,装饰器配置阶段不应随意执行耗时操作或业务副作用。

8.2 多个装饰器由下向上应用

复制代码
@first
@second
def target():
    ...

相当于:

复制代码
target = first(second(target))

second 先包装原函数,first 再包装结果。

8.3 调用时从外层进入

python 复制代码
from functools import wraps


def trace(label):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"{label}:进入")
            result = func(*args, **kwargs)
            print(f"{label}:退出")
            return result

        return wrapper

    return decorator


@trace("外层")
@trace("内层")
def work():
    print("执行业务")


if __name__ == '__main__':
    work()

运行结果:

复制代码
外层:进入
内层:进入
执行业务
内层:退出
外层:退出

需要区分两个阶段:

  • 装饰器表达式按代码中的顺序求值。
  • 得到的装饰器从下向上应用。
  • 调用包装后的函数时,从最外层进入,再逐层返回。

8.4 顺序会改变实际语义

例如计时与缓存:

复制代码
@timed
@lru_cache(maxsize=128)
def calculate(value):
    ...

计时位于外层,每次调用都会经过计时,包括缓存命中。

反过来:

复制代码
@lru_cache(maxsize=128)
@timed
def calculate(value):
    ...

缓存命中时,不会执行内部计时包装函数。

所以装饰器顺序不仅影响日志排列,还可能改变执行次数、性能统计和缓存行为。

9. 使用 lru_cache 缓存结果

9.1 为可复用计算增加缓存

functools.lru_cache 可以根据函数参数缓存返回结果。

python 复制代码
from functools import lru_cache


@lru_cache(maxsize=128)
def square(number):
    print(f"实际计算:{number}")
    return number ** 2


if __name__ == '__main__':
    print(square(5))
    print(square(5))
    print(square(6))
    print(square.cache_info())

运行结果:

复制代码
实际计算:5
25
25
实际计算:6
36
CacheInfo(hits=1, misses=2, maxsize=128, currsize=2)

第二次调用 square(5) 时,直接返回缓存结果,没有执行原函数体。maxsize=128 表示最多保留一定数量的最近使用缓存条目,并不是限制为 128 字节。

9.2 参数必须可哈希

下面的调用不适合直接缓存:

python 复制代码
@lru_cache(maxsize=128)
def total(numbers):
    return sum(numbers)


total([1, 2, 3])

因为列表不可哈希,会抛出 TypeError。可以根据业务将输入转换为元组:

python 复制代码
from functools import lru_cache


@lru_cache(maxsize=128)
def total(numbers):
    return sum(numbers)


if __name__ == '__main__':
    print(total((1, 2, 3)))

运行结果:

复制代码
6

不要为了满足缓存要求随意转换数据,仍需保证参数能够准确表达计算所依赖的状态。

9.3 清理缓存

复制代码
square.cache_clear()

可以清空缓存和命中统计。

还可以通过:

复制代码
square.cache_info()

查看使用情况。

lru_cache 默认没有按时间过期的 TTL,也不是跨进程共享缓存。

9.4 不适合缓存哪些函数

需要谨慎对待:

  • 依赖当前时间的函数。
  • 生成随机值的函数。
  • 读取持续变化外部状态的函数。
  • 带有必须执行的副作用的函数。
  • 每次需要返回独立可变对象的函数。
  • 生成器函数。

例如:

python 复制代码
from functools import lru_cache


@lru_cache(maxsize=8)
def get_tags(product_id):
    return ["饮料"]


if __name__ == '__main__':
    first = get_tags(1001)
    first.append("促销")

    print(get_tags(1001))

运行结果:

复制代码
['饮料', '促销']

缓存返回的是同一个结果对象,不会在每次命中时自动复制。

缓存生成器函数也容易出错,因为可能缓存的是同一个会被耗尽的生成器,而不是生成器产出的全部数据。

9.5 缓存实例方法也有边界

如果给实例方法添加 lru_cache:

  • self 会参与缓存键。
  • 实例需要可哈希。
  • 缓存可能保留对实例的引用。
  • 实例状态变化后,相同参数的缓存结果可能过期。

因此,不应把它直接作为所有对象方法的通用性能优化。

10. 装饰实例方法

10.1 普通包装函数通常可以接收 self

python 复制代码
from functools import wraps


def log_call(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"调用:{func.__name__}")
        return func(*args, **kwargs)

    return wrapper


class Inventory:
    def __init__(self, stock):
        self.stock = stock

    @log_call
    def restock(self, quantity):
        if type(quantity) is not int or quantity <= 0:
            raise ValueError("入库数量必须是正整数")

        self.stock += quantity
        return self.stock


if __name__ == '__main__':
    inventory = Inventory(10)

    print(inventory.restock(5))

运行结果:

复制代码
调用:restock
15

当包装后的普通函数放在类中,通过实例访问时,仍然会绑定实例。self 作为位置参数进入 args,再被转发给原方法。

10.2 与 classmethod、staticmethod 组合

对于本文这种接收普通函数的包装装饰器,可以把 classmethod 放在外层:

python 复制代码
class Product:
    def __init__(self, name):
        self.name = name

    @classmethod
    @log_call
    def from_name(cls, name):
        return cls(name)

应用顺序是:

复制代码
classmethod(log_call(原函数))

先包装普通函数,再交给 classmethod 建立类方法绑定。静态方法也可以采用:

python 复制代码
@staticmethod
@log_call
def normalize_name(name):
    return name.strip()

不同装饰器接收和返回的对象类型可能不同,不能假设所有装饰器都能任意交换位置。

相关推荐
编码者卢布2 小时前
【App Service 】WebJobs 多实例实验:谁在运行,什么时候运行?
开发语言·python
quantdash_cc2 小时前
Python 获取实时行情后如何进行批量筛选?从全市场快照到策略候选池
开发语言·python·数据分析·量化交易·股票数据·quantdash
小蜗 strong2 小时前
和电脑猜拳(随机程序应用)
服务器·前端·python
华研前沿标杆游学3 小时前
企业标杆游学|走进东莞OPPO总部✨探秘智造与品牌出海
python
大侠归来3 小时前
C语言内存管理:从栈到堆的完整指南
c语言·开发语言·python
LOVE️YOU4 小时前
Python 数据结构的本质:位置、对象引用、Hash 与可变性
数据结构·python·哈希算法
李航19834 小时前
自动动手开发图形引擎,不仅能AI建模,还能AI渲染
人工智能·python·计算机视觉·ai·ai编程
LeoCrawls4 小时前
Python 读取 JSON 常见报错排查,附完整处理函数
python·json·php
zzZ··*4 小时前
CodeBuddy 用量看板:本地解析 Token 与积分消耗,不联网不上传
python·vue·ai编程