当前位置:首页>python>第3.2章:快速学习Python的函数-进阶

第3.2章:快速学习Python的函数-进阶

  • 2026-10-11 07:01:07
第3.2章:快速学习Python的函数-进阶

《Python AI 应用开发入门》第 3.2 节。

使用可变参数、类型注解、文档字符串和 LEGB 作用域设计可复用函数。

本节目标

学完本节后,你应当能够:

  1. 1. 使用 *args 接收额外位置参数。
  2. 2. 使用 **kwargs 接收额外关键字参数。
  3. 3. 在调用函数时使用 * 和 ** 解包。
  4. 4. 为函数添加类型注解与文档字符串。
  5. 5. 在合适场景使用 Lambda。
  6. 6. 解释完整的 LEGB 名字查找顺序。
  7. 7. 理解 global 与 nonlocal 的作用和限制。
  8. 8. 设计可复用的 Prompt 构造函数。

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

含义:

  1. 1. required:必填位置或关键字参数。
  2. 2. default:带默认值的参数。
  3. 3. *args:其余位置参数。
  4. 4. enabled:*args 后的参数只能通过关键字传入。
  5. 5. **kwargs:其余关键字参数。

实际函数不要堆太多不同形式的参数。调用难以理解时,说明函数可能承担了过多职责。

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 为例:

  • • lambda:表示现在要创建一个函数。
  • • number:函数接收的参数。
  • • ::分隔参数和函数要计算的表达式。
  • • 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. 1. lambda message: ... 创建一个接收 message 参数的函数。
  2. 2. sorted() 取出一条消息,并调用这个函数。
  3. 3. 函数计算 len(message["content"]),返回消息内容的长度。
  4. 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 读取一个名字时,通常按以下顺序查找:

  1. 1. Local:当前函数局部作用域。
  2. 2. Enclosing:外层函数作用域。
  3. 3. Global:当前模块全局作用域。
  4. 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)

这里使用:

  • • 明确的必填参数 topic。
  • • *rules 接收任意数量规则。
  • • 只能用关键字传入的 role 和 style。
  • • 类型注解和文档字符串说明接口。
  • • 返回字符串,不在函数内部直接打印。

动手练习

练习 1:完善 Prompt 构造器

为 build_prompt() 增加:

  • • language 关键字参数。
  • • 忽略清理后为空的规则。
  • • 主题为空时给出明确失败结果;异常会在第 3.4 节系统学习。

练习 2:配置解包

创建字典:

options = {    "role": "代码审查助手",    "style": "直接",}

使用 **options 调用 Prompt 构造函数。

练习 3:观察 LEGB

分别在 Local、Enclosing 和 Global 位置创建同名变量,逐层删除并记录输出。再尝试用 nonlocal 修改 Enclosing 变量。

随堂小测

  1. 1. *args 在函数内部是什么类型?
  2. 2. **kwargs 在函数内部是什么类型?
  3. 3. 调用时 *values 和 **options 分别做什么?
  4. 4. 类型注解会在运行时自动拒绝错误类型吗?
  5. 5. 文档字符串应该放在函数体什么位置?
  6. 6. Lambda 最适合哪类逻辑?
  7. 7. LEGB 分别代表什么?
  8. 8. global 与 nonlocal 修改的范围有什么区别?
  9. 9. 为什么仍然优先推荐参数和返回值?

参考答案

  1. 1. 元组。
  2. 2. 字典。
  3. 3. 前者拆成位置参数,后者拆成关键字参数。
  4. 4. 不会,它主要提供说明并支持静态检查。
  5. 5. 函数体第一条语句。
  6. 6. 短小、单表达式且通常一次性的逻辑,例如排序键。
  7. 7. Local、Enclosing、Global、Built-in。
  8. 8. global 指向模块全局名字,nonlocal 指向最近外层函数的名字。
  9. 9. 它让数据依赖和修改结果显式可见,更容易测试和复用。

本节完成检查

  • • 我能使用 *args 和 **kwargs。
  • • 我能在调用时解包序列和字典。
  • • 我为函数添加了准确的类型注解。
  • • 我会编写简洁的文档字符串。
  • • 我只在短小表达式中使用 Lambda。
  • • 我能解释完整 LEGB 顺序。
  • • 我理解 global 与 nonlocal,但不会滥用。
  • • 我完成了可复用 Prompt 构造函数。

最新文章

随机文章