Python
控制流、函数与作用域
使用分支、模式匹配、参数约束和作用域设计边界明确、易于测试的函数。
发布于 2026年8月21日
控制流、函数与作用域
函数是 Python 项目最重要的设计边界。好的函数不仅复用代码,还把输入、输出、失败方式和副作用压缩成可理解的契约。
一、学习目标
- 使用分支、循环和模式匹配表达业务规则
- 设计位置参数、关键字参数和返回值
- 理解 LEGB 作用域和闭包
- 避免可变默认参数
- 将任务操作拆成可测试函数
二、分支与早返回
def normalize_title(value: str) -> str:
title = value.strip()
if not title:
raise ValueError("任务标题不能为空")
if len(title) > 200:
raise ValueError("任务标题不能超过 200 个字符")
return title
先处理非法条件可以减少嵌套。业务主路径留在函数的最低缩进层级,通常比多层 if 更容易阅读。
三、循环控制
for task in tasks:
if task["status"] == "done":
continue
print(task["title"])
break 提前结束循环,continue 跳到下一轮。循环还可以带 else,只有未被 break 中断时执行:
for task in tasks:
if task["id"] == wanted_id:
found = task
break
else:
raise KeyError(f"任务 {wanted_id} 不存在")
当查找逻辑频繁出现时,最好封装为函数或建立字典索引。
四、模式匹配
结构化模式匹配适合处理形状明确的命令:
def dispatch(command: tuple[str, ...]) -> str:
match command:
case ("add", title):
return f"新增:{normalize_title(title)}"
case ("done", raw_id) if raw_id.isdigit():
return f"完成:{int(raw_id)}"
case ("list",):
return "列出全部任务"
case _:
raise ValueError("未知命令")
match 不是简单的 switch。它可以解构序列、映射和类。不要为了使用新语法,把一个清楚的字典分派改成复杂匹配。
函数参数里的冒号不是“强制指定类型”,而是类型注解(type hint),Python 运行时不会自动检查类型,除非你自己写了检查逻辑。-> 是返回值注解。
五、参数设计
def list_tasks(
tasks: list[dict[str, object]],
/,
*,
status: str | None = None,
reverse: bool = False,
) -> list[dict[str, object]]:
selected = [
task for task in tasks
if status is None or task["status"] == status
]
return sorted(selected, key=lambda task: int(task["id"]), reverse=reverse)
/前是仅位置参数。仅位置参数是指只能按位置传入,不能用关键字名传参。
在函数定义里用斜杠 / 标记,例如:
def f(a, b, /, c, d=0):
...
上面 a、b 是仅位置参数:
- 合法:
f(1, 2, 3)、f(1, 2, c=3) - 非法:
f(a=1, b=2, c=3)会报TypeError
在上段代码里:
def list_tasks(tasks, /, *, status=None, reverse=False):
tasks 在 / 前,所以它是仅位置参数,必须这样调用:
list_tasks(tasks_list, status="done")✅list_tasks(tasks=tasks_list)❌
status和reverse在*后,是仅关键字参数,也就是只能按status=.../reverse=...传。
对外接口一旦发布,参数名称也可能成为兼容性的一部分。仅位置参数可以隐藏不重要的内部名称;仅关键字参数适合布尔开关和多个同类型参数。
六、返回值与失败
函数应明确区分三种情况:
- 正常结果:直接返回值。
- 可预期但无结果:按契约返回
None或空集合。 - 无法履行契约:抛出具体异常。
def find_task(tasks: list[dict[str, object]], task_id: int) -> dict[str, object] | None:
return next((task for task in tasks if task["id"] == task_id), None)
如果“不存在”是调用方必须处理的错误,可以改为抛出 KeyError。关键是同一层保持一致。
七、可变默认参数
错误示例:
def add_task(title: str, tasks: list[str] = []) -> list[str]:
tasks.append(title)
return tasks
默认参数只在函数定义时求值,多个调用会共享同一个列表。正确写法:
def add_task(title: str, tasks: list[str] | None = None) -> list[str]:
current = [] if tasks is None else tasks
current.append(title)
return current
如果函数不应修改调用方传入的列表,则先复制并返回新列表。
八、LEGB 作用域
名称按 Local、Enclosing、Global、Built-in 顺序查找:
prefix = "TASK"
def make_formatter(separator: str):
def format_task(task_id: int, title: str) -> str:
return f"{prefix}{separator}{task_id}: {title}"
return format_task
format_task 闭包引用外围函数的 separator。闭包适合保存少量配置;状态复杂时,类通常更直接。
避免在业务函数中修改全局可变状态。显式传入依赖更容易测试和并发执行。
九、函数是一等对象
from collections.abc import Callable
def select_tasks(
tasks: list[dict[str, object]],
predicate: Callable[[dict[str, object]], bool],
) -> list[dict[str, object]]:
return [task for task in tasks if predicate(task)]
todo = select_tasks(tasks, lambda task: task["status"] == "todo")
回调让算法和策略分离,但过多小型 lambda 会降低可读性。逻辑超过一个简单表达式时使用命名函数。
十、练习与自测
- 实现
complete_task(tasks, task_id),找不到时抛出KeyError。 - 为
list_tasks增加标题关键字筛选,并保持函数无副作用。 - 使用闭包创建带固定前缀的任务格式化器。
自测:
- 默认参数何时求值?
None返回值和异常分别适合什么情况?/与*如何约束调用方式?
十一、官方资料
上一篇:内置容器与推导式 | 下一篇:模块、包、虚拟环境与依赖