摘要:围绕一组聊天消息,讲清列表与字典的嵌套取值、
get()的默认值、for遍历、strip()清理、JSON 字符串与文件读写,以及*、**参数拆包。重点不是背方法名,而是判断当前数据是什么类型、代码执行后数据发生了什么变化。
所属专栏:大模型应用开发的 Python 工程基础 · 第 2 篇
适合读者:已了解变量、函数参数与返回值,但容易混淆列表、字典和 JSON 字符串的 Python 初学者。
运行环境:Python 3.10+,仅使用标准库。第一节和文件读写示例可以独立运行;其余片段说明各自需要的数据,不要把错误示例一起粘贴执行。
写聊天接口、处理模型消息或读取工具参数时,经常会看到这样的代码:
data["messages"][0]["content"]它看起来很长,其实只是逐层取值。本篇先处理一组聊天消息,再围绕这条流程拆解常用操作。不涉及模型调用、数据库或框架,让注意力集中在 Python 数据本身。
一、贯穿示例:读消息、清理内容、转回 JSON
假设收到一段 JSON 文本,需要读取每条消息,并清理内容两端的空白。
importjson# Python 自带的 JSON 模块,不需要额外安装。# 三个双引号用于书写多行字符串;raw 此时仍然是文本。raw=""" { "messages": [ {"role": "user", "content": " 你好 "}, {"role": "assistant", "content": " 有什么可以帮助你? "} ] } """data=json.loads(raw)# 解析 JSON;本例最外层是对象,结果是字典。messages=data["messages"]# 按键取出消息列表。formessageinmessages:# 每次取出一个消息字典,再执行缩进内的代码。message["content"]=message["content"].strip()# 清理并写回当前字典。first_content=messages[0]["content"]# 下标 0 表示第一个元素。request_id=data.get("request_id","未提供")# 缺少这个键时使用默认值。result=json.dumps(data,ensure_ascii=False)# 转为 JSON 字符串,保留中文显示。print(first_content)print(request_id)print(result)预期输出:
你好 未提供 {"messages": [{"role": "user", "content": "你好"}, {"role": "assistant", "content": "有什么可以帮助你?"}]}这段代码没有调用模型,也没有写文件。它完成的是:文本解析、读取数据、修改内容、再生成文本。
二、判断类型:看当前这一层,不要被里面的内容带偏
解析后的数据结构是:
data={"messages":[{"role":"user","content":"你好"},{"role":"assistant","content":"有什么可以帮助你?"},]}虽然里面有列表,但data本身是字典。沿着取值过程看:
| 表达式 | 本例中的类型 | 含义 |
|---|---|---|
data | 字典 | 整份数据,最外层是键值结构 |
data["messages"] | 列表 | 所有消息,按顺序保存 |
data["messages"][0] | 字典 | 第一条消息,有角色和内容两个字段 |
data["messages"][0]["content"] | 字符串 | 第一条消息的具体文字,即"你好" |
方括号在这里有两种用途:字典用键取值,列表用位置取元素。位置从 0 开始;列表为空时,不能直接取[0],否则会发生索引错误。
阅读嵌套数据的诀窍是:每取一层,就重新判断手里拿到的是什么。
不要只看到其中的[]就认定整份数据是列表,也不要认为json.loads()永远返回字典:JSON 对象通常解析成字典,JSON 数组解析成列表,其他 JSON 值也有各自对应的 Python 类型。
三、字典 get():缺键、None 和空字符串是三种情况
data={"message":"你好"}message=data["message"]# 已知键存在时,直接读取。request_id=data.get("request_id")# 没有这个键,返回 None。request_id=data.get("request_id","未提供")# 没有这个键,返回指定默认值。get()是字典自带的方法。第二个参数的含义是“键不存在时使用的值”,不是“内容为空时使用的值”。
| 字典内容 | data.get("request_id", "未提供")的结果 |
|---|---|
{} | "未提供" |
{"request_id": None} | None |
{"request_id": ""} | 空字符串"" |
没有传入默认值时,缺键默认返回None。参考:Python 官方字典方法
如果写data["request_id"]而键不存在,会抛出KeyError。不能简单地认为get()永远更好:可选字段可以缺失;必填字段缺失则需要明确检查和报错,不能用默认值悄悄掩盖。
四、遍历与清理:同样的处理,应用到每条消息
4.1 for 每次取出的是元素,不是序号
使用第一节中的messages列表:
formessageinmessages:print(message["role"],message["content"])第一次循环,message接收第一条消息字典;第二次接收第二条。每次执行相同的缩进代码,元素取完后循环结束。
message是自己起的变量名,不是 Python 固定要求的名字,也不是下标。这样写能处理不同数量的消息,不必手动写很多次messages[0]、messages[1]。
4.2 strip() 清理两端,并返回清理后的字符串
text=" hello world \n"clean_text=text.strip()print(clean_text)# hello world,中间的空格保留。默认的strip()去掉字符串两端的空白,包括空格、换行和制表符等;不删除中间的空格。参考:Python 官方字符串方法
最常见的错误是没有保存返回值:
text=" 你好 "text.strip()# 计算了结果,但没有接住;text 的内容不变。text=text.strip()# 保存清理结果,text 现在是 "你好"。strip()不会原地修改字符串。要修改消息字典中的字段,就把返回的字符串写回去:
# message 是当前循环取出的消息字典。message["content"]=message["content"].strip()在本文的普通列表遍历中,当前字典没有被复制,因此修改它,也会改变列表中对应的那条消息。这与上一篇“变量引用同一个对象”的理解相衔接。
业务上还要注意:只含空白的输入清理后会变成""。清理完成,不等于内容有效。代码、缩进文本等场景也不能不加判断地去掉两端空白,是否清理要看字段用途。
五、JSON 的四个方法:先问“字符串还是文件”
5.1 loads 与 dumps:处理字符串
importjson raw='{"message": "你好"}'# JSON 字符串。data=json.loads(raw)# 本例解析结果是字典。result=json.dumps(data,ensure_ascii=False)# 返回 JSON 字符串。print(data["message"])# 你好print(result)# {"message": "你好"}最外层包住整段内容的引号很重要:'{"message": "你好"}'是字符串,{"message": "你好"}是 Python 字典。
print(result)看起来像在显示字典,并不代表result是字典。打印字符串时,不会额外显示包住整个字符串的引号;要按键读取它,需要先解析。
ensure_ascii=False让中文直接显示,而不是转换成\uXXXX形式;必要的 JSON 转义仍然保留。它不是文件编码配置,文件编码要在打开文件时指定。
已经是字典的数据不需要再次loads()。JSON 的键需要双引号;"{'message': '你好'}"虽然像 Python 字典的文本表示,但不是合法 JSON 文本,不能直接交给loads()。
5.2 load 与 dump:读取或写入文件对象
下面是独立示例。若实际运行,会在当前工作目录创建message_demo.json;文件已经存在时会报错,不会覆盖。
importjson data={"message":"你好"}# open() 打开文件;"x" 创建新文件,已存在就报错。# encoding="utf-8" 指定文件编码。# with ... as file 把文件对象交给 file,代码块结束后自动关闭。withopen("message_demo.json","x",encoding="utf-8")asfile:json.dump(data,file,ensure_ascii=False)# 将 JSON 写入文件。# "r" 表示读取文件。withopen("message_demo.json","r",encoding="utf-8")asfile:restored=json.load(file)# 读取 JSON 并解析成 Python 数据。print(restored["message"])# 预期输出:你好。本节只使用with自动关闭文件,不展开资源管理机制。注意传给load()的是文件对象,不是文件名:json.load("message_demo.json")不符合这个接口的要求。
四个方法可以这样选:参考:Python 官方 JSON 文档
| 方法 | 接收什么 | 结果或作用 |
|---|---|---|
json.loads(raw) | JSON 字符串;也支持 JSON 字节数据 | 解析成对应的 Python 数据 |
json.dumps(data) | 可进行 JSON 编码的 Python 数据 | 返回 JSON 字符串 |
json.load(file) | 支持读取的文件对象 | 读取并解析 JSON |
json.dump(data, file) | 数据和支持写入的文本文件对象 | 将 JSON 写入文件,成功后返回None |
初学时可以记为:load读进来,dump写出去;带s的主要用于字符串形式。只在下一个环节需要不同形式时转换,不要反复把字典转成文本、再转回字典。
六、参数拆包:按名字传,还是按顺序传?
我们自己定义一个构造消息字典的函数,它不是 Python 或某个 SDK 自带的接口:
defcreate_message(content,role="user"):# content 必填;role 不传时使用默认值 "user"。return{"role":role,"content":content}6.1**字典:按键名传入参数
定义上面的函数后,可以这样调用:
data={"role":"user","content":"你好"}message=create_message(**data)# 等价于 create_message(role="user", content="你好")。**data将字典的键作为参数名、值作为参数值。对于本例,字典里两个键的先后顺序不影响对应关系。
不是“参数数量必须和字典键数一样”,而是:必填参数不能缺,带默认值的可以不传,提供的名字必须是函数接受的名字。
data={"content":"你好"}message=create_message(**data)# role 没有提供,使用默认值 "user"。对于这个没有接收额外关键字参数的普通函数,键改成text或多传request_id都会报错。Python 不会猜测text就是content。
6.2*列表:按位置依次传入参数
仍然使用本节开头的函数:
values=["你好","user"]message=create_message(*values)# 等价于 create_message("你好", "user")。# 第一个值传给 content,第二个值传给 role。*不只用于元组,也可以拆开列表等可迭代对象。本文先掌握列表这个常见情况即可。参考:Python 官方参数拆包教程
顺序反了,可能不报错,但业务含义会错:
values=["user","你好"]message=create_message(*values)print(message)# {'role': '你好', 'content': 'user'}该函数没有检查角色是否合法,只会按参数位置接收。角色、内容这种有明确含义的字段,按名称传参通常更容易审查。
*、**是调用时的传参方式,不是 JSON 解析,也不会替你完成业务校验。不要把未经检查的外部大字典直接拆包给任意函数。
七、常见错误对照
| 表现 | 错误思路或写法 | 正确判断 |
|---|---|---|
| 想按键取值,却把数据当成了列表 | 看到内部有[],认定整个data是列表 | 看当前层;本文最外层是字典 |
| 缺键时报错 | 对可缺失字段直接用data["request_id"] | 根据业务使用get()或明确校验 |
| 默认值没有生效 | 认为键的值为None或""时会使用默认值 | get()的默认值只在缺键时使用 |
| 内容两端的空格还在 | 调用strip()但不保存返回值 | 保存结果或写回字段 |
| JSON 解析失败 | 把 Python 字典的单引号文本当作 JSON | 使用合法 JSON,或对已有字典直接操作 |
| 把写文件结果当成文本 | 用result = json.dump(...)获取 JSON 字符串 | 需要字符串时使用dumps() |
| 拆包传参报错 | 字典的键不符合函数参数要求 | 必填、默认值和接收的名字一起检查 |
| 函数执行成功但内容错了 | *列表的元素顺序反了 | 按位置核对,必要时改为按名称传参 |
八、综合练习与验收
使用第一节的完整示例,先手动判断,再在方便时运行:
data、messages、result分别是什么类型?first_content和request_id分别是什么?- 如果循环内只写
message["content"].strip(),原字典的内容会改变吗? - 如果直接把
data写入已经打开的文件,选择dump()还是dumps()? - 调用
create_message(**{"content": "你好"})时,为什么缺少role仍然可以执行?
参考答案:
- 第 1 题:字典、列表、字符串。每个变量的类型由它当前接收的数据决定。
- 第 2 题:
"你好"和"未提供"。 - 第 3 题:不会。清理结果没有写回。
- 第 4 题:
dump(),接收数据和文件对象。 - 第 5 题:
role有默认值;缺少必填的content则不能这样调用。
验收不是背出方法名称,而是能解释每一步拿到什么、修改了哪里,以及参数为什么能对应。手动回答通过说明概念与代码阅读理解;实际动手还要验证正确示例,并能修复错误案例。
九、总结
处理数据时,先判断类型,再选择操作。列表按位置组织元素,字典按键组织字段;嵌套结构需要逐层判断。get()处理缺键,strip()返回清理后的字符串,JSON 方法根据“文本还是文件”选择,参数拆包则区分“按位置”和“按名称”。
先把这条小流程读懂,后续处理请求、聊天历史和工具参数时,才不容易把文本、字典、列表和函数参数混在一起。
你最容易混淆的是嵌套取值,还是 JSON 的四个方法?欢迎在评论区交流你的判断过程。