《Python AI 应用开发入门》第 3.2 节。
使用可变参数、类型注解、文档字符串和 LEGB 作用域设计可复用函数。
本节目标
学完本节后,你应当能够:
- 2. 使用
**kwargs 接收额外关键字参数。 - 7. 理解
global 与 nonlocal 的作用和限制。
1. *args 接收多个位置参数
参数数量不固定时,可以使用 *args:
def join_rules(*rules): return "\n".join(rules)text = join_rules( "回答不超过 100 字", "给出一个示例", "不确定时明确说明",)print(text)
函数内部的 rules 是元组:
def inspect_args(*args): print(type(args)) print(args)
args 只是惯用名称,星号才决定“收集额外位置参数”。
普通参数可以放在前面:
def build_rule_list(title, *rules): lines = [title] for index, rule in enumerate(rules, start=1): lines.append(f"{index}. {rule}") return "\n".join(lines)
不要因为方便就让所有函数都使用 *args。固定且有明确含义的数据仍应使用命名参数。
2. **kwargs 接收多个关键字参数
def show_options(**options): print(type(options)) print(options)show_options(model="demo-model", temperature=0.7, streaming=False)
函数内部的 options 是字典:
{ "model": "demo-model", "temperature": 0.7, "streaming": False,}
可以组合普通参数:
def build_metadata(source, **fields): metadata = {"source": source} metadata.update(fields) return metadata
**kwargs 适合扩展选项,但它也会让允许哪些字段变得不明显。核心配置应尽量写成明确参数。
3. 参数顺序
常见顺序是:
def example(required, default="value", *args, enabled=True, **kwargs): pass
含义:
- 4.
enabled:*args 后的参数只能通过关键字传入。
实际函数不要堆太多不同形式的参数。调用难以理解时,说明函数可能承担了过多职责。
4. 调用时解包
星号在调用位置表示把序列拆成位置参数:
def display_message(role, content): print(f"[{role}] {content}")message_values = ("user", "你好")display_message(*message_values)
双星号把字典拆成关键字参数:
message_data = { "role": "assistant", "content": "你好!",}display_message(**message_data)
字典键必须与参数名匹配,否则会触发 TypeError。
5. 类型注解
类型注解说明函数期望和返回的数据:
def normalize_role(role: str) -> str: return role.strip().lower()
容器注解:
def count_messages(messages: list[dict[str, str]]) -> int: return len(messages)
可能返回 None:
def find_last_user_message( messages: list[dict[str, str]],) -> dict[str, str] | None: for message in reversed(messages): if message["role"] == "user": return message return None
类型注解不会自动阻止错误类型:
def double(value: int) -> int: return value * 2print(double("A")) # 仍会运行,并得到 "AA"
它主要帮助人、编辑器和类型检查工具理解代码。运行时校验仍需要自己编写。
6. 文档字符串
函数体第一条字符串通常作为文档字符串:
def build_prompt(topic: str, style: str = "简洁") -> str: """根据主题和回答风格构造练习 Prompt。""" return f"请用{style}风格解释:{topic}"
复杂函数可以说明参数、返回值和异常:
def require_non_empty(text: str) -> str: """清理文本并确保结果非空。 Args: text: 需要清理的原始文本。 Returns: 去除两端空白后的文本。 Raises: ValueError: 清理后的文本为空。 """ cleaned = text.strip() if not cleaned: raise ValueError("文本不能为空") return cleaned
下一节会看到模块和 IDE 如何利用这些文档。
7. Lambda 的适用场景
7.1 什么是 Lambda
lambda 是 Python 用来创建简短函数的关键字。它创建的函数没有通过 def 声明函数名,因此通常称为匿名函数。
先看一个普通函数:
def double(number): return number * 2print(double(5)) # 10
使用 lambda 可以把相同逻辑写成一个表达式:
double = lambda number: number * 2print(double(5)) # 10
Lambda 的基本语法是:
lambda 参数1, 参数2, ...: 返回值表达式
以 lambda number: number * 2 为例:
- •
number * 2:调用函数时计算的表达式。
Lambda 不需要写 return。冒号后表达式的计算结果会被自动返回:
add = lambda first, second: first + secondresult = add(3, 4)print(result) # 7
这里的 add 是一个变量,它保存了 Lambda 创建的函数。虽然可以这样使用,但如果函数需要命名并重复调用,普通的 def 通常更清晰:
def add(first, second): return first + second
Lambda 的优势不是让函数更强大,而是可以在需要一个简单函数的位置,直接写出这个函数。
7.2 把 Lambda 传给另一个函数
假设需要按照消息内容的长度排序。先用普通函数实现:
messages = [ {"role": "user", "content": "短消息"}, {"role": "assistant", "content": "这是一条更长的回答"},]def content_length(message): return len(message["content"])sorted_messages = sorted( messages, key=content_length,)
sorted() 的 key 参数需要接收一个函数。排序时,sorted() 会依次把每条消息传给 content_length(),然后使用函数返回的长度进行比较。
这个辅助函数只有一个表达式,而且只在这里使用一次,因此可以改写为 Lambda:
sorted_messages = sorted( messages, key=lambda message: len(message["content"]),)
执行 key=lambda message: len(message["content"]) 时,可以按下面的过程理解:
- 1.
lambda message: ... 创建一个接收 message 参数的函数。 - 2.
sorted() 取出一条消息,并调用这个函数。 - 3. 函数计算
len(message["content"]),返回消息内容的长度。 - 4.
sorted() 根据每条消息得到的长度完成排序。
这里并不是立即调用 Lambda,而是先把 Lambda 创建的函数交给 sorted()。真正的调用由 sorted() 在排序过程中完成。
7.3 什么时候使用 Lambda
Lambda 适合同时满足以下条件的逻辑:
常见场景包括给 sorted() 提供排序键,或者给 min()、max() 提供比较依据:
longest_message = max( messages, key=lambda message: len(message["content"]),)print(longest_message)
如果逻辑需要多步计算、循环、异常处理、类型说明或文档字符串,就应该使用普通函数:
def content_length(message: dict[str, str]) -> int: content = message["content"].strip() if not content: return 0 return len(content)
Lambda 不是“更高级”或“运行更快”的普通函数。它只是适合表达短小、一次性函数的紧凑语法;当紧凑写法影响可读性时,优先使用 def。
8. LEGB 名字查找顺序
Python 读取一个名字时,通常按以下顺序查找:
- 4. Built-in:Python 内置作用域。
示例:
label = "Global"def outer(): label = "Enclosing" def inner(): label = "Local" print(label) inner()outer()
输出 Local。删除内层赋值后会找到 Enclosing,再删除外层赋值才会找到 Global。
9. 嵌套函数与闭包
外层函数可以返回内层函数:
def create_prefixer(prefix: str): def add_prefix(text: str) -> str: return f"{prefix}{text}" return add_prefixuser_formatter = create_prefixer("[user] ")print(user_formatter("你好"))
add_prefix() 记住了外层调用中的 prefix。这种保留外层变量的函数叫闭包。
当前只需要理解 Enclosing 作用域,不必为了使用闭包而把普通代码变复杂。
10. global
global 声明函数中的名字对应模块全局变量:
message_count = 0def add_one(): global message_count message_count += 1
它可以工作,但让函数暗中修改外部状态。调用者只看参数和返回值无法知道影响。
更推荐:
def add_one(message_count: int) -> int: return message_count + 1
global 适合极少数确实需要模块级共享状态的场景,不应该作为解决作用域报错的第一选择。
11. nonlocal
nonlocal 声明函数中的名字对应外层函数变量,可以理解为 global 的"局部版":
def create_counter(): count = 0 def increment(): nonlocal count count += 1 return count return incrementcounter = create_counter()print(counter())print(counter())
输出:
12
nonlocal 不会查找模块全局变量,它只针对 Enclosing 作用域。
普通数据处理优先使用参数和返回值。只有闭包确实需要保留并更新状态时才考虑 nonlocal。
12. 可复用的 Prompt 构造函数
def build_prompt( topic: str, *rules: str, role: str = "Python 助教", style: str = "简洁",) -> str: """构造包含角色、主题、风格和规则的 Prompt。""" cleaned_topic = topic.strip() if not cleaned_topic: raise ValueError("主题不能为空") lines = [ f"你是一名{role}。", f"请用{style}风格解释:{cleaned_topic}", ] if rules: lines.append("回答要求:") for index, rule in enumerate(rules, start=1): lines.append(f"{index}. {rule.strip()}") return "\n".join(lines)
调用:
prompt = build_prompt( "生成器", "使用初学者能理解的语言", "提供一个代码示例", style="循序渐进",)print(prompt)
这里使用:
- • 只能用关键字传入的
role 和 style。
动手练习
练习 1:完善 Prompt 构造器
为 build_prompt() 增加:
- • 主题为空时给出明确失败结果;异常会在第 3.4 节系统学习。
练习 2:配置解包
创建字典:
options = { "role": "代码审查助手", "style": "直接",}
使用 **options 调用 Prompt 构造函数。
练习 3:观察 LEGB
分别在 Local、Enclosing 和 Global 位置创建同名变量,逐层删除并记录输出。再尝试用 nonlocal 修改 Enclosing 变量。
随堂小测
- 3. 调用时
*values 和 **options 分别做什么? - 8.
global 与 nonlocal 修改的范围有什么区别?
参考答案
- 6. 短小、单表达式且通常一次性的逻辑,例如排序键。
- 7. Local、Enclosing、Global、Built-in。
- 8.
global 指向模块全局名字,nonlocal 指向最近外层函数的名字。 - 9. 它让数据依赖和修改结果显式可见,更容易测试和复用。
本节完成检查
- • 我理解
global 与 nonlocal,但不会滥用。