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']
结合场景再看三个关注点
- 装饰器不改变业务返回值时,应透明转发
*args/**kwargs与返回结果。 wraps让日志、OpenAPI、测试收集看到真名place_order。- 可变默认参数是定义时绑定:默认用
None+ 内部新建。
核心概念与准确模型
1. 函数对象
def执行时创建函数对象并绑定名字。- 可作为参数/返回值,挂属性,放入容器。
2. 参数形态
| 形态 | 含义 |
|---|---|
| 位置/关键字参数 | 常规绑定 |
| 默认参数 | 定义时求值 |
*args | 多余位置参数元组 |
**kwargs | 多余关键字参数字典 |
/ 与 * | 仅位置 / 仅关键字分界(3.x) |
3. 闭包
- 内层函数可读取外层变量。
- 捕获的是变量,不是定义瞬间的值快照(除非默认参数技巧)。
- 写外层变量用
nonlocal。
4. 装饰器
@dec等价于f = dec(f)@dec(x)等价于f = dec(x)(f)- 叠加时从下到上包装
5. lambda
- 单表达式匿名函数;复杂逻辑用
def以利调试。
边界情况与反直觉行为
- 早绑定默认参数 vs 晚绑定闭包循环变量(循环里
lambda: i经典坑)。 - 生成器函数
yield改变调用返回值类型(生成器对象)——见迭代器篇。 - 异步函数
async def返回 coroutine——见异步篇。
常见误区
[!warning] 常见误区:装饰器只是“注解” 错误理解:
@像 Java 注解不改变运行。
正确模型:它是替换可调用对象的语法糖,运行时真实包装。
[!warning] 常见误区:默认
[]/{}很方便 错误理解:每次调用都给新容器。
正确模型:默认只求值一次,可变对象会跨调用共享。
工程实践
- 公共函数补类型标注与文档字符串。
- 副作用(IO、时间)通过参数注入,便于测试。
- 装饰器保持幂等与透明;需要状态时明确线程/异步安全。
- 拒绝超长
**kwargs黑洞接口,优先显式参数。
本节总结
函数是 Python 组织行为的核心单元;装饰器是横切能力的惯用组装方式。掌握绑定、默认值、闭包与 wraps,才能进入 OOP 与异步而不慌。
自测题
- 为什么
bad2会包含'a'? - 无
wraps时place_order.__name__通常变成什么?
参考答案
- 默认列表在函数定义时创建一次,两次调用共享同一 list。
- 常变成
wrapper(内层函数名),导致日志/文档失真。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| Tutorial – Defining Functions | 教程 | 函数基础 |
| functools.wraps | 文档 | 装饰器元数据 |
| Compound statements – function | 规范 | 定义语义 |