10.3 读陌生代码的方法
目标
- 掌握一套固定的读码流程:入口与全貌 → 核心数据 → 名字来源 → 实验验证。
- 熟练使用
type、dir、help、vars、inspect在解释器中探查对象。 - 会用
breakpoint()在运行中暂停查看变量。
核心概念
四步读码法
| 步骤 | 做什么 | 关注 |
|---|---|---|
| 1. 入口与全貌 | 读文件头文档字符串、README 的运行命令;扫一遍 import、顶层常量、函数/类列表;找主流程(main()、if __name__ == "__main__"、顶层循环) | 这个文件“做什么”“从哪里开始执行” |
| 2. 核心数据 | 找出关键变量,记录它在哪里创建、修改、读取 | 数据形状(list/dict 嵌套)与流向 |
| 3. 名字来源 | 遇到不认识的名字,判断来自:局部 / 全局 / 导入(标准库或第三方)/ 内置 | 去对应文档或源码查 |
| 4. 实验验证 | 把片段放进 REPL 或本站运行器;加 print;用 breakpoint() 暂停 | 用运行结果验证你的理解 |
读的时候先跳过细节:第一遍只看控制流骨架(for/if/return/break),把每个块用一句话概括。
探查对象的工具
Python · 可运行
python
import inspect
import json
value = json.loads('{"type": "tool_use", "input": {"city": "上海"}}')
print(type(value), type(value["input"])) # 它是什么类型?
print([n for n in dir(str) if not n.startswith("_")][:8]) # 它有哪些属性/方法?
print(inspect.signature(json.dumps)) # 函数接受哪些参数?
print(inspect.getdoc(json.loads).splitlines()[0]) # 文档第一行
print(json.dumps.__module__, inspect.getsourcefile(json.dumps)) # 定义在哪里?| 工具 | 回答的问题 |
|---|---|
type(x) | x 是什么类型? |
dir(x) | x 有哪些属性和方法? |
help(x) | x 的文档(REPL 中分页显示) |
vars(obj) / obj.__dict__ | 实例自身的属性及值 |
inspect.signature(f) | 函数签名 |
inspect.getsource(f) / getsourcefile(f) | 源码 / 源码文件 |
x.__module__ | 定义所在模块 |
callable(x) | 能否调用 |
追踪数据:画出状态表
对核心变量,在每个关键时刻记录它的值。下面用一个模拟的模型回复序列,打印 minimal_agent.py 中 messages 的演变:
Python · 可运行
python
replies = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "t1", "name": "get_weather", "input": {"city": "上海"}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": "上海今天晴,22°C。"}]},
]
messages = [{"role": "user", "content": "上海今天天气怎么样?"}]
def summary(msgs):
parts = []
for m in msgs:
c = m["content"]
kind = c if isinstance(c, str) else ",".join(b["type"] for b in c)
parts.append(f"{m['role']}:{kind}")
return " | ".join(parts)
print("开始 ", summary(messages))
for turn, resp in enumerate(replies, 1):
messages.append({"role": "assistant", "content": resp["content"]})
if resp["stop_reason"] != "tool_use":
print(f"第{turn}轮后", summary(messages), "→ break")
break
results = [{"type": "tool_result", "tool_use_id": b["id"], "content": "上海:晴,22°C"}
for b in resp["content"] if b["type"] == "tool_use"]
messages.append({"role": "user", "content": results})
print(f"第{turn}轮后", summary(messages))暂停程序:breakpoint()
在本地代码中插入 breakpoint(),运行到这里会进入 pdb 调试器:
python
for block in resp.content:
breakpoint() # 运行到这里暂停
...常用命令:p 表达式(打印)、pp(美化打印)、n(下一行)、s(进入函数)、c(继续)、l(显示附近代码)、w(调用栈)、q(退出)。VS Code/PyCharm 的图形化断点功能相同。(浏览器运行器不支持交互式调试,请在本地 uv run 中使用。)
用 ast 快速列出结构
标准库 ast 可以把源码解析成语法树,适合快速获得“全貌”:
Python · 可运行
python
import ast
source = '''
import os
import anthropic
from dotenv import load_dotenv
MAX_TURNS = 5
def get_weather(city):
return city
'''
tree = ast.parse(source)
for node in tree.body:
if isinstance(node, ast.Import):
print("import ", [a.name for a in node.names])
elif isinstance(node, ast.ImportFrom):
print("from-import ", node.module, [a.name for a in node.names])
elif isinstance(node, ast.FunctionDef):
print("def ", node.name, [a.arg for a in node.args.args])
elif isinstance(node, ast.Assign):
print("assign ", [t.id for t in node.targets if isinstance(t, ast.Name)])易错点
- 从第一行逐字读到最后一行:效率低。先骨架、后细节;先主流程、后辅助函数。
- 只读不跑:对任何不确定的行为,花 30 秒写个最小片段运行一下。
- 忽略数据形状:大多数“看不懂”来自不清楚某个变量是 list 还是 dict、里面装的是什么。先
print(type(x))、print(x)。 - 在第三方库源码里迷路:先看公开 API 的签名与文档字符串,只在需要时下钻到
_开头的内部模块。
对照
| 需求 | Python | JS(Node) | Java |
|---|---|---|---|
| 查类型 | type(x) | typeof x / x.constructor.name | x.getClass() |
| 列属性 | dir(x) | Object.keys(x) / 原型链 | 反射 getMethods() |
| 断点 | breakpoint() | debugger; | IDE 断点 |
| 交互式探查 | REPL + help() | Node REPL / DevTools | jshell |
| 跳转定义 | IDE / inspect.getsourcefile | IDE | IDE |
读代码
用四步法读 minimal_agent.py:
- 入口与全貌:文档字符串说明“最小 Agent 循环”;导入
os、anthropic、load_dotenv;顶层常量PROVIDERS、MAX_TURNS;无main(),主流程是顶层的for turn in range(...)循环。 - 核心数据:
messages(创建于循环前,每轮追加 assistant 与 user 两条)、resp(每轮模型回复)、results(工具结果列表)。 - 名字来源:
client.messages.create、resp.stop_reason、block.type来自anthropicSDK;get_weather、TOOL_FUNCTIONS是本文件定义的。 - 实验验证:本站 11.1 提供了一个“假客户端”,可以在浏览器中运行完整循环并观察
messages的变化。
练习
练习 1:列出导入的名字
实现 imported_names(source):用 ast 解析源码,返回一个 dict:{当前文件中可用的名字: 来源模块}。规则:import a.b → {"a": "a"};import a as x → {"x": "a"};from m import n → {"n": "m"};from m import n as y → {"y": "m"}。只处理模块顶层语句。
练习
import ast
def imported_names(source):
tree = ast.parse(source)
return {}
快捷键:⌘/Ctrl + Enter 提交检查
练习 2:必填参数
实现 required_params(func):用 inspect.signature 返回函数中没有默认值的参数名列表(忽略 *args、**kwargs)。
练习
import inspect
def required_params(func):
return list(inspect.signature(func).parameters)
快捷键:⌘/Ctrl + Enter 提交检查
小结
- 四步法:入口与全貌 → 核心数据 → 名字来源 → 实验验证。
type/dir/help/inspect回答“是什么、有什么、怎么调、在哪定义”。- 为核心变量画状态表;不确定就运行;
breakpoint()暂停查看。