浏览知识库目录

Python

控制流、函数与作用域

使用分支、模式匹配、参数约束和作用域设计边界明确、易于测试的函数。

控制流、函数与作用域

函数是 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):
    ...

上面 ab 是仅位置参数:

  • 合法: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)
    statusreverse* 后,是仅关键字参数,也就是只能按 status=... / reverse=... 传。

对外接口一旦发布,参数名称也可能成为兼容性的一部分。仅位置参数可以隐藏不重要的内部名称;仅关键字参数适合布尔开关和多个同类型参数。


六、返回值与失败

函数应明确区分三种情况:

  1. 正常结果:直接返回值。
  2. 可预期但无结果:按契约返回 None 或空集合。
  3. 无法履行契约:抛出具体异常。
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 会降低可读性。逻辑超过一个简单表达式时使用命名函数。


十、练习与自测

  1. 实现 complete_task(tasks, task_id),找不到时抛出 KeyError
  2. list_tasks 增加标题关键字筛选,并保持函数无副作用。
  3. 使用闭包创建带固定前缀的任务格式化器。

自测:

  • 默认参数何时求值?
  • None 返回值和异常分别适合什么情况?
  • /* 如何约束调用方式?

十一、官方资料

上一篇:内置容器与推导式 | 下一篇:模块、包、虚拟环境与依赖