Course Builder个人学习工作台 · 本地模式
Modern Python:面向有经验开发者的回归路线

函数签名、参数与作用域

目标:能够设计清楚的位置参数、关键字参数和返回值契约,并避免可变默认值与隐式全局状态。

Python 函数调用比“把值按顺序塞进参数”更丰富。参数可以分为:

  • positional-only:写在 / 之前,调用者不能依赖参数名;
  • positional-or-keyword:最常见;
  • keyword-only:写在 * 之后,调用时必须写名字;
  • *args:额外位置参数打包成 tuple;
  • **kwargs:额外关键字参数打包成 dict。

*args/**kwargs 适合透明转发和确实开放的接口,不应拿来隐藏一个本可明确写出的签名。对公共函数,名字清楚的 keyword-only 参数往往比多个 bool 位置参数更可读。

完整示例:构建可组合处理管线

保存为 functions_lab.py:

from __future__ import annotations

from collections.abc import Callable

Transform = Callable[[str], str]


def strip_text(text: str) -> str:
    return text.strip()


def truncate(text: str, /, *, width: int = 20, suffix: str = "…") -> str:
    if width < len(suffix):
        raise ValueError("width must fit suffix")
    if len(text) <= width:
        return text
    return text[: width - len(suffix)] + suffix


def apply_pipeline(text: str, *steps: Transform) -> str:
    result = text
    for step in steps:
        result = step(result)
    return result


if __name__ == "__main__":
    result = apply_pipeline(
        "  这是一条很长的客户消息  ",
        strip_text,
        lambda value: truncate(value, width=10),
    )
    print(result)
    assert len(result) == 10

运行:

uv run python functions_lab.py

预期输出:

这是一条很长的客户…

实际汉字数量以字符串字符计数;这里不是字节截断。text 被 / 设为 positional-only,因为它的名字对调用者没有配置意义;width 和 suffix 是 keyword-only,让 truncate(value, 10, "...") 这种难读调用无法出现。

默认参数何时求值

默认值在函数定义时求值一次,而不是每次调用时求值:

# 错误示范
# def collect(value: str, bucket: list[str] = []) -> list[str]:
#     bucket.append(value)
#     return bucket

应使用 None sentinel:

def collect(value: str, bucket: list[str] | None = None) -> list[str]:
    target = [] if bucket is None else bucket
    target.append(value)
    return target

这样“调用者没提供容器”和“调用者明确提供空容器”是两个可区分状态。

作用域规则

名字查找通常按 Local → Enclosing → Global → Builtins。给名字赋值会让它默认成为当前局部名字;这就是为什么在函数里先读后写同名全局变量会出现 UnboundLocalError。相比 global,更好的默认是把状态作为参数传入、作为结果返回,或封装进对象。

引导实验:让接口拒绝含糊调用

尝试运行:

truncate("abcdef", 4)

你会得到 TypeError,因为 width 必须按名传入。改成 truncate("abcdef", width=4) 即可。这个错误不是限制表达力,而是在调用点保存参数含义。

独立练习

实现 make_prefixer(prefix: str) -> Transform,返回一个闭包;把它加入 pipeline,使结果以 [INFO] 开头。

参考实现
def make_prefixer(prefix: str) -> Transform:
    def add_prefix(text: str) -> str:
        return f"{prefix}{text}"

    return add_prefix


result = apply_pipeline(" hello ", strip_text, make_prefixer("[INFO] "))
assert result == "[INFO] hello"

闭包捕获的是 enclosing scope 中的 prefix 绑定。这里没有共享可变全局状态,每次工厂调用都得到独立配置。

排错提示

  • 多次调用结果累积:检查是否用了可变默认参数。
  • UnboundLocalError:寻找函数内对同名变量的赋值;优先改成参数/返回值。
  • lambda 变得复杂:改用有名字的 def,便于测试、类型检查和 traceback。

参考资料