函数签名、参数与作用域
目标:能够设计清楚的位置参数、关键字参数和返回值契约,并避免可变默认值与隐式全局状态。
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。