当前位置:首页>python>第4.4章:速览Python的常用高级语法

第4.4章:速览Python的常用高级语法

  • 2026-10-03 07:30:57
第4.4章:速览Python的常用高级语法

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

使用解包与模式匹配处理结构化数据,理解装饰器和上下文管理器的基本运行机制。

本节目标

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

  1. 1. 使用序列解包把数据分配给多个变量。
  2. 2. 使用 * 收集剩余元素。
  3. 3. 使用 ** 合并字典并向函数传递关键字参数。
  4. 4. 使用 match / case 处理结构化命令。
  5. 5. 解释 case 模式、通配分支和守卫条件。
  6. 6. 说明函数为什么可以作为值传递和返回。
  7. 7. 展开并解释 @decorator 语法。
  8. 8. 编写不会丢失参数、返回值和异常的简单装饰器。
  9. 9. 解释 with 进入和离开代码块时发生了什么。
  10. 10. 使用类或 contextlib.contextmanager 创建简单上下文管理器。

1. 序列解包

当一组数据的位置固定、含义明确时,可以按索引逐项取出:

command = ("add", "user", "你好")action = command[0]role = command[1]content = command[2]

不过,变量名和索引分开书写,不容易一眼看出三项数据的对应关系。序列解包可以在一次赋值中把各项交给对应变量:

action, role, content = commandprint(action)   # addprint(role)     # userprint(content)  # 你好

可以把它理解为:Python 把右侧序列拆开,再按照位置交给等号左侧的变量。因此两边数量必须一致:

# action, role = command# ValueError: too many values to unpack

列表、元组和字符串等可以逐项读取的对象都能参与解包:

first, second = ["user", "assistant"]letter_a, letter_b = "AI"

解包适合结构固定、每个位置含义明确的数据。如果长度不确定,直接固定多个变量接收就容易报错。

2. 使用 * 收集剩余元素

命令内容可能包含多个单词:

parts = ["add", "user", "请", "解释", "数据类"]action, role, *content_parts = partsprint(action)         # addprint(role)           # userprint(content_parts)  # ['请', '解释', '数据类']

action 和 role 分别接收前两项,带 * 的 content_parts 会把剩余所有元素收集成一个列表:

content = " ".join(content_parts)print(content)  # 请 解释 数据类

也可以收集开头或中间部分:

first, *middle, last = [1, 2, 3, 4, 5]print(first)   # 1print(middle)  # [2, 3, 4]print(last)    # 5

一次解包中只能有一个带 * 的变量,否则 Python 无法判断“剩余元素”应该交给谁。

3. 交换变量与忽略数据

解包可以交换变量:

current_role = "user"next_role = "assistant"current_role, next_role = next_role, current_role

如果某个位置的值不需要使用,通常用 _ 接收:

role, _, content = ("user", "message-001", "你好")

Python 并没有为 _ 赋予“删除这个值”的特殊能力,它仍然是普通变量名。这里的下划线只是告诉读者“这个位置的值后面不会用到”,因此不要再依赖 _ 保存重要数据。

4. 字典解包

项目配置常常由“默认配置”和“用户配置”两部分组成。** 可以把两个字典的键值对展开到一个新字典中:

default_config = {    "model": "demo-model",    "style": "简洁",    "max_history": 50,}user_config = {    "style": "详细",    "max_history": 20,}config = {    **default_config,    **user_config,}print(config)

Python 按书写顺序合并这些键值对。遇到同名键时,后面的值覆盖前面的值,所以这里的用户配置会覆盖默认配置中的 style 和 max_history。

合并配置时,书写顺序就是覆盖规则,要先确认谁应该拥有更高优先级。密码、权限等安全设置也不应交给未经验证的输入任意覆盖。

5. 调用函数时解包

上一章在函数定义中见过 *args 和 **kwargs。在函数调用的位置,* 和 ** 做的是相反方向的工作:把已有容器拆开,再传给函数。

def create_message(role, content):    return {        "role": role,        "content": content,    }values = ("user", "解释解包")message = create_message(*values)

字典键可以对应参数名:

values = {    "role": "user",    "content": "解释字典解包",}message = create_message(**values)

create_message(*values) 会按顺序传入元组中的两项;create_message(**values) 会把字典键当作参数名、字典值当作参数值。

因此要区分两个位置:

  • • 定义函数时,*args 和 **kwargs 用来收集参数。
  • • 调用函数时,*values 和 **values 用来展开参数。

6. 用 match / case 选择处理方式

多个命令可以使用 if / elif:

if action == "add":    print("添加消息")elif action == "list":    print("查看消息")elif action == "exit":    print("退出")

当判断的不只是一个值,还包括列表或字典的结构时,连续的 if / elif 会逐渐变得难读。Python 3.10 及以上提供了结构化模式匹配:

match action:    case "add":        print("添加消息")    case "list":        print("查看消息")    case "exit":        print("退出")    case _:        print("未知命令")

match 后面是要检查的数据,每个 case 描述一种可能的值或结构。Python 从上到下检查,执行第一个匹配的分支。case _ 不限制具体值,因此用作最后的默认分支。

match 不需要替代所有 if。判断年龄是否大于 18、字符串是否为空等真假条件,仍然更适合使用 if;match 更适合根据固定值或数据结构选择处理方式。

7. 匹配并提取序列数据

模式匹配不仅能判断命令类型,还能顺便把命令中的数据取出来:

def handle_command(command):    parts = command.split()    match parts:        case ["list"]:            return "查看全部消息"        case ["add", role, *content_parts] if content_parts:            content = " ".join(content_parts)            return f"添加 {role} 消息:{content}"        case ["recent", amount_text]:            return f"查看最近 {amount_text} 条"        case ["exit"]:            return "退出程序"        case _:            return "命令格式无效"

逐个看这些模式:

  • • ["list"] 只匹配一个元素且内容是 list 的序列。
  • • ["add", role, *content_parts] 匹配以 add 开头的序列,并提取角色与剩余内容。
  • • if content_parts 是守卫条件。序列结构匹配后,还要确认内容列表不是空的。
  • • 模式不匹配时继续检查下一个 case。

模式中的 role 和 content_parts 是接收数据的新变量,不是要拿来比较的固定值,因此不需要提前定义。

8. 匹配字典结构

JSON 消息解析后会得到字典。字典模式可以检查必需的键是否存在,同时取出对应的值:

def describe_message(value):    match value:        case {"role": "user", "content": content}:            return f"用户:{content}"        case {"role": "assistant", "content": content}:            return f"助手:{content}"        case {"role": role, "content": content}:            return f"{role}:{content}"        case _:            raise ValueError("消息结构无效")

例如第一条模式要求 role 的值必须是 "user",并把 content 对应的值交给同名变量。字典模式只要求写出来的键存在,原字典可以包含其他额外键。

模式匹配能确认键和值的大致结构,却不会自动检查 content 是不是为非空字符串。仍然需要进一步验证:

def describe_non_empty_message(value):    match value:        case {"role": role, "content": content} if isinstance(content, str) and content.strip():            return f"{role}:{content.strip()}"        case _:            raise ValueError("消息内容无效")

9. 函数也可以作为值使用

理解装饰器前,先回顾一个关键点:定义函数后,函数名保存的是一个可以调用的函数对象。它也能像字符串、列表一样,被赋给变量、作为参数传入,或者从另一个函数返回。

函数可以赋给变量:

def greet(name):    return f"你好,{name}"formatter = greetprint(formatter("小林"))

可以作为参数传入:

def run_formatter(formatter, value):    return formatter(value)print(run_formatter(greet, "小林"))

formatter = greet 没有执行 greet,因为后面没有括号;它只是让 formatter 也指向这个函数。直到写出 formatter("小林") 时,函数才真正执行。

函数也可以从另一个函数返回。上一章学习闭包时已经使用过这种能力,装饰器正是在此基础上工作的。

10. 为什么需要装饰器

假设多个业务函数都要在调用前后记录日志。如果把记录代码直接写进每个函数,真正的业务步骤很快会被重复代码包围:

def save_history():    print("开始调用 save_history")    print("正在保存")    print("结束调用 save_history")

装饰器把这类重复步骤集中到一个地方:它接收原函数,创建一个负责附加行为的新函数,再把新函数返回。

最小装饰器:

def announce(function):    def wrapper(*args, **kwargs):        print(f"开始调用 {function.__name__}")        result = function(*args, **kwargs)        print(f"结束调用 {function.__name__}")        return result    return wrapper

手动使用:

def build_prompt(topic):    return f"请解释:{topic}"build_prompt = announce(build_prompt)print(build_prompt("装饰器"))

这段代码发生了三件事:

  1. 1. 原来的 build_prompt 被传给 announce()。
  2. 2. announce() 返回内部定义的 wrapper。
  3. 3. 变量 build_prompt 改为指向 wrapper,而 wrapper 仍然记得并会调用原函数。

因此再次调用 build_prompt("装饰器") 时,实际先进入 wrapper,打印开始信息,调用原函数,最后打印结束信息并返回原结果。

11. @ 装饰器语法

下面的写法:

@announcedef build_prompt(topic):    return f"请解释:{topic}"

等价于:

def build_prompt(topic):    return f"请解释:{topic}"build_prompt = announce(build_prompt)

@announce 写在函数定义上方,表示函数定义完成后,立即执行 build_prompt = announce(build_prompt)。它只是更简洁的语法,不是一套完全不同的运行机制。

装饰器常见用途包括:

  • • 记录函数调用。
  • • 测量执行时间。
  • • 检查权限。
  • • 缓存结果。
  • • 统一处理接口边界。

只有当某种附加行为会稳定地用于一批函数时,装饰器才有价值。如果额外步骤只服务于一个函数,或会隐藏重要的执行顺序,直接写清楚通常更容易维护。

12. 使用 functools.wraps

前面的装饰器还有一个小问题:装饰后变量指向的是 wrapper,所以函数名、文档字符串等信息也会变成包装器的信息:

print(build_prompt.__name__)  # wrapper

标准库 functools.wraps 会把原函数的重要信息复制到包装器上:

from functools import wrapsdef announce(function):    @wraps(function)    def wrapper(*args, **kwargs):        print(f"开始调用 {function.__name__}")        result = function(*args, **kwargs)        print(f"结束调用 {function.__name__}")        return result    return wrapper

因此,编写函数装饰器时通常要在包装器上加上 @wraps(function)。

一个不改变原函数基本用法的包装器还应该:

  • • 把位置和关键字参数继续传给原函数。
  • • 返回原函数的结果。
  • • 不随意吞掉原函数异常。

13. with 为什么能自动清理资源

前一章使用:

with path.open("r", encoding="utf-8") as file:    content = file.read()

文件使用完后必须关闭。即使 file.read() 报错,with 也会负责执行关闭操作。能够配合 with 完成这类准备与清理工作的对象,叫作上下文管理器。

上下文管理器负责两个阶段:

  1. 1. 进入代码块前准备资源。
  2. 2. 离开代码块时清理资源,即使代码块中途发生异常。

一个类可以通过两个特殊方法支持 with:

  • • __enter__():进入时调用,它的返回值交给 as 后的变量。
  • • __exit__():退出时调用,接收异常信息并执行清理。

14. 类形式的上下文管理器

文件对象已经实现了上下文管理器。下面再自己写一个简单的操作记录器,观察 with 的执行过程:

class OperationTrace:    def __init__(self, operation_name):        self.operation_name = operation_name    def __enter__(self):        print(f"开始:{self.operation_name}")        return self    def __exit__(self, error_type, error, traceback):        if error is None:            print(f"完成:{self.operation_name}")        else:            print(f"失败:{self.operation_name},原因:{error}")        return False

使用:

with OperationTrace("保存聊天历史") as trace:    print(trace.operation_name)    print("正在保存……")

正常执行顺序:

  1. 1. 创建 OperationTrace 对象。
  2. 2. 调用 __enter__()。
  3. 3. 执行 with 内部代码。
  4. 4. 调用 __exit__()。

如果代码块发生异常,Python 会把异常类型、异常对象和追踪信息传给 __exit__(),所以它仍有机会完成清理和记录。这里返回 False,表示错误没有被解决,应该继续向外传播。只有上下文管理器确实已经处理并恢复了错误时,才应考虑返回 True。

15. 函数形式的上下文管理器

如果上下文管理器只需要简单的“准备—执行—清理”流程,可以使用标准库的 contextlib.contextmanager,不必专门定义一个类:

from contextlib import contextmanager@contextmanagerdef operation_trace(operation_name):    print(f"开始:{operation_name}")    try:        yield    except Exception as error:        print(f"失败:{operation_name},原因:{error}")        raise    else:        print(f"完成:{operation_name}")

使用:

with operation_trace("生成回答"):    print("正在生成……")

装饰器会把这个生成器函数转换成可供 with 使用的上下文管理器。可以把 yield 看成代码块的分界线:

  • • yield 前:进入上下文时执行。
  • • 执行到 yield:暂时停下,转去运行 with 代码块。
  • • 代码块结束后:回到 yield 后面继续执行。

如果需要保存较多状态、返回专门对象或提供额外方法,类形式更清楚;只有少量进入和退出步骤时,函数形式通常更简洁。

16. 装饰器和上下文管理器的区别

装饰器和上下文管理器都能在业务代码周围加入通用行为,但它们包围的范围不同:

工具
主要作用范围
典型用途
装饰器
某个函数的每次调用
调用日志、权限、缓存
上下文管理器
明确的代码块
文件、锁、临时状态、操作追踪

如果只想管理某几行代码,with 的范围一眼可见;如果一个函数每次被调用时都必须执行同样的附加行为,装饰器更合适。

17. 综合示例:处理命令

from contextlib import contextmanager@contextmanagerdef operation_trace(name):    print(f"开始:{name}")    try:        yield    finally:        print(f"结束:{name}")def parse_command(command):    parts = command.split()    match parts:        case ["add", role, *content_parts] if content_parts:            return "add", {                "role": role,                "content": " ".join(content_parts),            }        case ["recent", amount_text]:            return "recent", {"amount_text": amount_text}        case ["exit"]:            return "exit", {}        case _:            raise ValueError("命令格式无效")with operation_trace("解析命令"):    action, arguments = parse_command("add user 解释上下文管理器")print(action)print(arguments)

这个例子没有为了使用新语法而改变业务含义:

  • • match 提取命令结构。
  • • 序列解包接收函数返回值。
  • • 上下文管理器清楚标出需要追踪的操作范围。

所谓“高级语法”并不是越多越好。只有当一种写法能减少重复、让数据结构或执行范围更清楚时,才值得使用。

常见错误

解包数量不匹配

固定解包前先确认数据结构;长度变化时使用 *rest 或显式检查。

match 分支顺序错误

宽泛模式放在前面会让后面更具体的模式永远没有机会匹配。通配分支通常放最后。

装饰器忘记返回结果

包装器不返回原函数结果,会让调用者得到 None。

装饰器吞掉异常

记录错误后通常应使用 raise 继续传播,除非装饰器明确负责恢复。

上下文管理器隐藏异常

__exit__() 返回真值会表示异常已处理。不能恢复时返回 False。

动手练习

练习 1:命令解包与匹配

编写 parse_command(command),支持:

  • • add <role> <content...>。
  • • recent <amount>。
  • • list。
  • • exit。

返回动作名和参数字典,非法结构抛出 ValueError。

练习 2:调用记录装饰器

编写 log_call:

  • • 调用前打印函数名。
  • • 原样传递参数。
  • • 返回原函数结果。
  • • 使用 @wraps。
  • • 原函数异常继续传播。

练习 3:操作上下文

分别使用类和 @contextmanager 实现操作追踪,测试正常结束和主动抛出异常两种情况,确认清理信息都会输出。

随堂小测

  1. 1. first, *middle, last 中 middle 是什么类型?
  2. 2. 字典解包出现同名键时使用哪个值?
  3. 3. case _ 的作用是什么?
  4. 4. 守卫条件在什么时候检查?
  5. 5. @announce 与哪段普通赋值代码等价?
  6. 6. 为什么装饰器包装器要返回原函数结果?
  7. 7. @wraps 解决什么问题?
  8. 8. __enter__ 的返回值会交给哪里?
  9. 9. __exit__ 返回 False 表示什么?
  10. 10. 装饰器和上下文管理器的作用范围有什么区别?

参考答案

  1. 1. 列表。
  2. 2. 后展开或后写入的值。
  3. 3. 匹配前面分支没有处理的任何值,通常作为默认分支。
  4. 4. 结构模式匹配成功后、执行分支代码前。
  5. 5. function = announce(function)。
  6. 6. 否则包装后函数会丢失原结果,调用者得到 None。
  7. 7. 保留原函数名称、文档等元数据。
  8. 8. as 后面的变量。
  9. 9. 上下文管理器没有吞掉异常,异常应继续传播。
  10. 10. 装饰器通常覆盖某个函数的每次调用;上下文管理器覆盖明确的代码块。

最新文章

随机文章