Python 函数与装饰器

Python 函数是一等对象:参数绑定、默认值求值时机、闭包与装饰器语法糖;工程上如何写可测试的可调用边界。

#type / concept #status / growing #resource / python #tech / lang / python #tech / dev

[!info] 关联笔记

Python 函数与装饰器

这个概念为什么出现

Python 把行为做成可传递的对象:回调、策略、中间件、装饰器都依赖“函数是一等公民”。只记住 def 语法但不知道:

  • 参数如何绑定
  • 默认值何时求值
  • 闭包捕获的是变量
  • 装饰器只是高阶函数语法糖

就无法读懂现代库代码,也写不好可测试边界。

[!abstract] 一句话理解 函数是可调用对象:调用时绑定参数;默认参数在定义时求值;闭包按名字捕获外围变量;装饰器用函数包装函数并常经 @ 语法应用。

最小可运行示例

先把示例放进业务场景,再看代码:

场景:订单服务给下单函数加审计日志与重试策略

下单核心函数应保持纯粹;审计日志与简单重试通过装饰器叠加,避免每个业务函数复制样板。

# order_decorators.py
# 业务意图:对 place_order 增加调用审计,并演示参数/闭包/wraps。
# 教学点:
# - *args/**kwargs 转发;
# - 装饰器返回 wrapper;
# - functools.wraps 保留原函数元数据;
# - 可变默认参数陷阱对照。

from __future__ import annotations

import functools
from typing import Any, Callable


def audit(action: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    # 带参数装饰器:外层接收配置,内层接收函数。
    def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
        @functools.wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            print(f"[audit] action={action} call={func.__name__}")
            result = func(*args, **kwargs)
            print(f"[audit] action={action} done")
            return result

        return wrapper

    return decorator


@audit("create_order")
def place_order(user_id: str, sku: str, qty: int = 1) -> dict:
    # 业务:创建订单文档(演示用 dict)。
    if qty <= 0:
        raise ValueError("qty must be positive")
    return {"user_id": user_id, "sku": sku, "qty": qty}


def append_tag_bad(tag: str, bucket: list[str] = []) -> list[str]:
    # 反例:可变默认参数在定义时创建一次,跨调用共享。
    bucket.append(tag)
    return bucket


def append_tag_good(tag: str, bucket: list[str] | None = None) -> list[str]:
    if bucket is None:
        bucket = []
    bucket.append(tag)
    return bucket


def main() -> None:
    print(place_order("u1", "SKU-1"))
    print("name preserved:", place_order.__name__)
    print("bad1:", append_tag_bad("a"))
    print("bad2:", append_tag_bad("b"))  # 看到共享列表
    print("good1:", append_tag_good("a"))
    print("good2:", append_tag_good("b"))


if __name__ == "__main__":
    main()

建议运行:

python order_decorators.py

期望输出:

[audit] action=create_order call=place_order
[audit] action=create_order done
{'user_id': 'u1', 'sku': 'SKU-1', 'qty': 1}
name preserved: place_order
bad1: ['a']
bad2: ['a', 'b']
good1: ['a']
good2: ['b']

结合场景再看三个关注点

  1. 装饰器不改变业务返回值时,应透明转发 *args/**kwargs 与返回结果。
  2. wraps 让日志、OpenAPI、测试收集看到真名 place_order
  3. 可变默认参数是定义时绑定:默认用 None + 内部新建。

核心概念与准确模型

1. 函数对象

  • def 执行时创建函数对象并绑定名字。
  • 可作为参数/返回值,挂属性,放入容器。

2. 参数形态

形态含义
位置/关键字参数常规绑定
默认参数定义时求值
*args多余位置参数元组
**kwargs多余关键字参数字典
/*仅位置 / 仅关键字分界(3.x)

3. 闭包

  • 内层函数可读取外层变量。
  • 捕获的是变量,不是定义瞬间的值快照(除非默认参数技巧)。
  • 写外层变量用 nonlocal

4. 装饰器

  • @dec 等价于 f = dec(f)
  • @dec(x) 等价于 f = dec(x)(f)
  • 叠加时从下到上包装

5. lambda

  • 单表达式匿名函数;复杂逻辑用 def 以利调试。

边界情况与反直觉行为

  1. 早绑定默认参数 vs 晚绑定闭包循环变量(循环里 lambda: i 经典坑)。
  2. 生成器函数 yield 改变调用返回值类型(生成器对象)——见迭代器篇。
  3. 异步函数 async def 返回 coroutine——见异步篇。

常见误区

[!warning] 常见误区:装饰器只是“注解” 错误理解:@ 像 Java 注解不改变运行。
正确模型:它是替换可调用对象的语法糖,运行时真实包装。

[!warning] 常见误区:默认 []/{} 很方便 错误理解:每次调用都给新容器。
正确模型:默认只求值一次,可变对象会跨调用共享。

工程实践

  • 公共函数补类型标注与文档字符串。
  • 副作用(IO、时间)通过参数注入,便于测试。
  • 装饰器保持幂等与透明;需要状态时明确线程/异步安全。
  • 拒绝超长 **kwargs 黑洞接口,优先显式参数。

本节总结

函数是 Python 组织行为的核心单元;装饰器是横切能力的惯用组装方式。掌握绑定、默认值、闭包与 wraps,才能进入 OOP 与异步而不慌。

自测题

  1. 为什么 bad2 会包含 'a'
  2. wrapsplace_order.__name__ 通常变成什么?
参考答案
  1. 默认列表在函数定义时创建一次,两次调用共享同一 list。
  2. 常变成 wrapper(内层函数名),导致日志/文档失真。

延伸阅读与资料来源

资料类型支撑内容
Tutorial – Defining Functions教程函数基础
functools.wraps文档装饰器元数据
Compound statements – function规范定义语义
创建于 2026/3/24 更新于 2026/7/15