01-函数是一等公民
Python 版本要求:Python 3.11+
贯穿项目:Web API 请求处理系统
本节目标:理解函数的四大能力,为装饰器学习打下基础
代码位置:decorators_demo/app/decorators/ch01_first_class.py
测试验证:uv run pytest tests/test_ch01.py -v
概念铺垫
1. 为什么需要函数进阶?
问题场景:API 路由分发
假设你正在构建一个 Web API,需要根据不同的 URL 路径调用不同的处理函数:
# ❌ 方案一:硬编码 if-elif 分支
def handle_request(path: str, data: dict) -> dict:
if path == "/users":
return handle_users(data)
elif path == "/products":
return handle_products(data)
elif path == "/orders":
return handle_orders(data)
# 每次新增路由都要修改这个函数!违反开闭原则
# ✅ 方案二:函数作为数据结构
routes = {
"/users": handle_users,
"/products": handle_products,
"/orders": handle_orders,
}
def handle_request(path: str, data: dict) -> dict | None:
handler = routes.get(path)
if handler is None:
return None
return handler(data) # 直接调用函数变量核心问题:如何把函数当作普通值来传递、存储、返回?
2. 生活类比:快递分拣中心
把函数想象成快递包裹:
┌─────────────────────────────────────────────────────────────┐
│ 快递分拣中心 — 函数是一等公民 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 普通变量 = 普通包裹 │
│ • 存放数据(数字、字符串、列表) │
│ • 可以赋值、传递、存储 │
│ │
│ 函数 = 特殊包裹 │
│ • 存放"处理逻辑" │
│ • 同样可以赋值、传递、存储、返回 │
│ │
│ 分拣流程: │
│ ───────────────── │
│ 1. 赋值 = 把包裹放到货架上 │
│ say_hello = greet │
│ │
│ 2. 传参 = 把包裹交给另一个处理站 │
│ execute(greet, "Alice") │
│ │
│ 3. 返回 = 处理站产出新包裹 │
│ return get_operation("add") │
│ │
│ 4. 存储 = 把包裹分类放进储物柜 │
│ routes = {"/users": handle_users} │
│ │
└─────────────────────────────────────────────────────────────┘一句话:
函数是一等公民,可以像数据一样被赋值、传递、存储、返回
L1 理解层:会用
3. 函数是一等公民的四大能力
能力 1:赋值给变量
def greet(name: str) -> str:
return f"Hello, {name}!"
# 函数可以赋值给变量(是引用,不是拷贝)
say_hello: Callable[[str], str] = greet
say_hello("Alice") # Hello, Alice!
say_hello is greet # True — 指向同一对象关键点:赋值创建的是引用,不是副本。say_hello 和 greet 指向同一个函数对象。
能力 2:作为参数传递
def execute(func: Callable[[str], str], value: str) -> str:
"""接收一个函数作为参数并调用它"""
return func(value)
execute(greet, "World") # Hello, World!这是高阶函数的核心特征:接收函数作为参数的函数。
能力 3:作为返回值
def get_operation(op: str) -> Callable[[int, int], int]:
"""根据操作名返回对应的计算函数"""
operations = {
"add": lambda a, b: a + b,
"sub": lambda a, b: a - b,
"mul": lambda a, b: a * b,
}
return operations.get(op, lambda _a, _b: 0)
add = get_operation("add")
add(3, 5) # 8函数可以动态生成并返回,这是工厂模式和装饰器的基础。
能力 4:存储在数据结构中
routes: dict[str, Callable[[dict], dict]] = {
"/users": handle_users,
"/products": handle_products,
"/orders": handle_orders,
}
def dispatch(path: str, req: dict) -> dict | None:
handler = routes.get(path)
if handler is None:
return None
return handler(req)这是策略模式和路由系统的核心:用数据结构代替条件分支。
4. 贯穿实战:路由分发系统
架构图解
请求:GET /users
│
↓
┌─────────────┐
│ 路由表 │ routes = {
│ (dict) │ "/users": handle_users,
│ │ "/products": handle_products,
│ │ "/orders": handle_orders
│ │ }
└─────────────┘
│
↓
routes["/users"](request)
│
↓
┌─────────────┐
│ handle_users│ 执行处理函数
│ (函数) │
└─────────────┘
│
↓
返回:{"users": ["Alice", "Bob", "Charlie"]}代码实现
完整代码见 app/decorators/ch01_first_class.py,核心逻辑:
# 定义处理函数
def handle_users(_req: dict[str, Any]) -> dict[str, list[str]]:
return {"users": ["Alice", "Bob", "Charlie"]}
def handle_products(_req: dict[str, Any]) -> dict[str, list[str]]:
return {"products": ["Apple", "Banana"]}
def handle_orders(_req: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
return {"orders": [{"id": 1, "user": "Alice"}]}
# 路由表:路径 → 处理函数
routes: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {
"/users": handle_users,
"/products": handle_products,
"/orders": handle_orders,
}
# 分发器
def dispatch(path: str, req: dict[str, Any]) -> dict[str, Any] | None:
handler = routes.get(path)
if handler is None:
return None
return handler(req)关键代码说明:
| 代码 | 含义 | 为什么这样写 |
|---|---|---|
routes: dict[str, Callable] | 路径→函数映射 | 函数作为字典值存储 |
handler = routes.get(path) | 查找处理函数 | 函数作为变量取出 |
return handler(req) | 调用处理函数 | 函数作为变量调用 |
运行测试验证:
cd decorators_demo
uv run pytest tests/test_ch01.py -v5. 深入理解:Callable 类型注解
Callable 的含义
Callable[[参数类型...], 返回值类型]Callable[[str], str]:接收一个str参数,返回str的函数Callable[[int, int], int]:接收两个int参数,返回int的函数Callable[[], None]:无参数,无返回值的函数
常见用法
from collections.abc import Callable
from typing import Any
# 变量类型注解
say_hello: Callable[[str], str] = greet
# 函数参数类型注解
def execute(func: Callable[[str], str], value: str) -> str:
return func(value)
# 函数返回值类型注解
def get_operation(op: str) -> Callable[[int, int], int]:
...
# 复杂签名:接收 dict 返回 dict
routes: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {...}注意:Callable 在 Python 3.9+ 中也可以从 typing 导入,但 collections.abc.Callable 是更推荐的做法。
L2 实践层:用好
推荐做法
| 做法 | 原因 | 示例 |
|---|---|---|
| 用函数字典替代 if-elif | 更简洁、可扩展,符合开闭原则 | routes = {"/users": handler} |
使用 Callable 类型注解 | 明确函数签名,IDE 可提供类型检查 | routes: dict[str, Callable[[dict], dict]] |
用 collections.abc.Callable | 比 typing.Callable 更推荐(3.9+) | from collections.abc import Callable |
| 函数引用不调用时不加括号 | 区分"传递函数"和"调用函数" | routes["/users"] = handle_users 而非 handle_users() |
用 lambda 写简单逻辑 | 避免为一次性函数单独命名 | lambda a, b: a + b |
反模式:不要这样做
# ❌ 错误:字典存储函数时带括号(会立即调用)
routes = {
"/users": handle_users(), # NameError: handle_users 未定义 / 传参缺失
}
# ✅ 正确:传递函数引用,不调用
routes = {
"/users": handle_users,
}
# ❌ 错误:健忘地修改了所有路由的分发逻辑
def dispatch(path, req):
if path == "/users":
return handle_users(req)
elif path == "/products":
return handle_products(req)
# ...每次新增路由都要改函数
# ✅ 正确:用字典路由表
def dispatch(path, req):
handler = routes.get(path)
if handler:
return handler(req)
# ❌ 错误:在循环中定义 lambda 并遗漏参数绑定
multipliers = [lambda x: x * i for i in range(5)] # 全部返回 x * 4
# ✅ 正确:用默认参数捕获当前值
multipliers = [lambda x, i=i: x * i for i in range(5)]适用场景
| 场景 | 是否推荐 | 原因 |
|---|---|---|
| Web 路由分发 | ✅ 推荐 | 路径→处理函数的字典天然适合路由 |
| 回调函数注册 | ✅ 推荐 | 将函数作为参数传递,解耦调用方和实现方 |
| 策略模式 | ✅ 推荐 | 用函数字典替代 if-elif 实现多态 |
| 简单条件分支(2-3 个) | ⚠️ 可选 | 少量分支用 if-elif 更直观 |
| 需要修改分发逻辑 | ❌ 不推荐 | 函数字典适合静态映射,动态逻辑需要其他方式 |
L3 专家层:深入
Python 如何实现:函数对象的内部结构
在 CPython 中,def 语句创建的是一个 PyFunctionObject,它包含一组关键内部属性:
┌─────────────────────────────────────────────────────────────┐
│ PyFunctionObject 内部结构 │
│ │
│ def greet(name): 定义函数 │
│ return f"Hello, {name}!" │
│ │
│ greet 对象包含: │
│ ┌──────────────────┐ │
│ │ __code__ │ → PyCodeObject │
│ │ • co_argcount=1 │ (参数个数) │
│ │ • co_varnames │ ("name",) (局部变量名) │
│ │ • co_code │ b't\x00|\x00...' (字节码指令) │
│ │ • co_consts │ ('Hello, ', None) (常量表) │
│ │ • co_names │ ('format',) (全局名) │
│ ├──────────────────┤ │
│ │ __globals__ │ → {'greet': ..., ...} (全局命名空间) │
│ ├──────────────────┤ │
│ │ __defaults__ │ → None (默认参数值) │
│ ├──────────────────┤ │
│ │ __closure__ │ → None (闭包 cell 元组,自由函数为 None)│
│ ├──────────────────┤ │
│ │ __name__ │ → "greet" │
│ ├──────────────────┤ │
│ │ __qualname__ │ → "greet" │
│ ├──────────────────┤ │
│ │ __doc__ │ → None │
│ ├──────────────────┤ │
│ │ __annotations__ │ → {'name': <class 'str'>, ...} │
│ └──────────────────┘ │
│ │
│ 赋值 say_hello = greet 时: │
│ • 不会复制 PyFunctionObject │
│ • say_hello 和 greet 指向同一个对象(引用计数 +1) │
│ • say_hello is greet → True │
│ │
└─────────────────────────────────────────────────────────────┘验证代码:
def greet(name: str) -> str:
return f"Hello, {name}!"
say_hello = greet
# 引用语义:指向同一对象
print(say_hello is greet) # True
print(id(say_hello) == id(greet)) # True
# 内部结构探索
print(greet.__code__) # <code object greet at ...>
print(greet.__code__.co_argcount) # 1
print(greet.__code__.co_varnames) # ('name',)
print(greet.__code__.co_names) # ('format',) ← 调用了什么全局名
print(greet.__code__.co_consts) # ('Hello, ', None) ← 常量表
print(greet.__closure__) # None(自由函数没有闭包)
print(greet.__defaults__) # None(没有默认参数)字节码层面:调用 greet("World") 实际执行的是 CALL 指令:
import dis
def greet(name: str) -> str:
return f"Hello, {name}!"
dis.dis(greet)
# 输出:
# 2 0 RESUME 0
# 3 2 LOAD_CONST 1 ('Hello, ')
# 4 LOAD_FAST 0 (name)
# 6 FORMAT_VALUE 0
# 8 LOAD_CONST 2 (None)
# 10 BUILD_STRING 2
# 12 RETURN_VALUE调用链:CALL 指令 → 创建新帧(frame)→ 执行字节码 → RETURN_VALUE → 销毁帧。
性能考量
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
func() 调用 | O(1) | 通过函数指针直接跳转,开销 ~40ns |
routes.get(path) 查找 | O(1) 平均 | 字典哈希查找 |
lambda 创建 | O(1) | 轻量级函数对象,无状态 |
| 大型 if-elif 分发 | O(n) | n 个分支依次比较,性能随分支数下降 |
| 方式 | 10 个路由 | 100 个路由 | 1000 个路由 |
|---|---|---|---|
if-elif 分发 | ~50ns | ~200ns | ~2000ns |
字典分发 routes.get() | ~40ns | ~40ns | ~40ns |
结论:字典路由分发在分支数 > 5 时性能优势明显,且代码更简洁。
知识关联
┌─────────────────────────────────────────────────────────────┐
│ 知识关联图:函数是一等公民 → 后续知识 │
│ │
│ 第 1 章:函数是一等公民 │
│ ┌──────────────────────────────────────┐ │
│ │ • 函数可赋值、传参、返回、存储 │ │
│ │ • Callable 类型注解 │ │
│ │ • __code__、__closure__ 等内部属性 │ │
│ └──────────┬───────────────────────────┘ │
│ │ │
│ ┌────────┼────────┬──────────────┐ │
│ ↓ ↓ ↓ ↓ │
│ 第2章 第3章 第5章 第6章 │
│ 闭包 装饰器 partial/wraps ParamSpec │
│ "函数 "函数 "固定函数 "保留函数 │
│ 内引用 替换" 部分参数" 精确签名" │
│ 外层" │
│ │
│ Python 标准库中的"函数是一等公民"用法: │
│ ───────────────────────────────── │
│ • sorted(data, key=len) → 函数作为参数传递 │
│ • list(map(str.upper, data))→ 函数作为参数传递 │
│ • signal.signal(SIGINT, handler)→ 函数注册为回调 │
│ • @app.route("/")(view) → 函数作为装饰器参数 │
│ │
└─────────────────────────────────────────────────────────────┘延伸知识点:
- 函数是对象:
type(greet)返回<class 'function'>,函数是function类的实例 __call__协议:任何实现了__call__的对象都可以像函数一样调用,这是类装饰器的基础inspect模块:inspect.signature(),inspect.getsource(),inspect.ismethod()等用于运行时检查函数
6. 自检清单
回答以下问题,检查你是否掌握了核心概念:
- 函数赋值给变量后,新变量和原函数是同一个对象吗?
execute(greet, "World")中,execute是什么类型的函数?get_operation("add")返回的是什么?- 为什么用函数字典代替 if-elif 分支?
Callable[[int, int], int]表示什么含义?
答案:
- 是同一个对象,赋值创建的是引用而非副本(
say_hello is greet为True) - 高阶函数 — 接收函数作为参数的函数
- 一个函数对象(lambda),接收两个 int 返回 int,具体是加法函数
- 更简洁、易扩展、符合开闭原则(新增路由不需要修改分发逻辑)
- 接收两个
int参数,返回int的函数类型
7. 本章能力清单
学完本章,你能够:
- [x] 将函数赋值给变量并理解引用语义
- [x] 将函数作为参数传递给高阶函数
- [x] 将函数作为返回值实现函数工厂
- [x] 用函数字典实现路由分发系统
- [x] 理解
Callable类型注解的含义