结构即模式:Python 结构与模式语法解码
本文面向有 Java 背景、会写基础 Python、想读懂框架源码的开发者。示例来自 AgentScope 2.0.4 与 myagent 项目。
一、为什么结构语法难读:从能写到能读框架源码
你写过 Python,也能跑通基础脚本。但当你打开 AgentScope / myagent 源码,看到 def spawn(coro, *, session_id, ...)、asyncio.gather(*tasks)、match case,会不会卡住?
这些语法不是花哨特性,而是 Python 处理"复杂结构"的原语——它们让数据结构的分拣、展开、重组更声明式。看懂它们,你就能读懂框架源码里大量"处理数据"的代码。
这一篇覆盖五类结构/模式语法,每类配一个 Java 对比锚点:用 match/case 按结构分拣数据(Java 增强 switch 的升级版)、用 *args/**kwargs 接收不定参数(Java varargs 的推广)、用关键字强制 * 让函数签名更安全(Java 无对应)、用调用解包 f(*seq)/f(**d) 展开序列和字典(Java 数组展开)、用推导式声明式地变换 list/dict/set(Java Stream 的近亲)。
前置要求不高:会定义函数、用 list/dict/set,了解 Java varargs(String...)作为对比锚点即可。本文不涉及异步(留待系列后续篇)。
二、模式匹配:match/case —— Java 增强 switch 的升级版
Python 3.10 引入 match/case(结构化模式匹配),它能按结构分拣数据,而不仅是按值。
从 Java 增强 switch 说起
Java 的 switch(含增强 switch + pattern matching)按值/模式分派:
// Java 21 增强 switch + 记录模式
return switch (shape) {
case Circle c -> "圆,半径 " + c.radius();
case Rect r -> "矩形 " + r.w() + "x" + r.h();
default -> "未知形状";
};
Python 的 match/case 做类似的事,但更彻底——它能解构数据结构:
# Python 3.10 match/case
def describe(shape):
match shape:
case {"kind": "circle", "radius": r}:
return f"圆,半径 {r}"
case {"kind": "rect", "w": w, "h": h}:
return f"矩形 {w}x{h}"
case _:
return "未知形状"
关键区别:按"结构"匹配,不是按"值"
Java switch: match 一个值 / 模式
Python match/case: match 一个"结构",同时解构出内部字段
Python 的 case {"kind": "circle", "radius": r} 不仅判断"这是个 dict",还同时把 radius 的值绑定到变量 r。这是声明式的"分拣 + 解构"。
AgentScope 真实案例
AgentScope 里 match/case 常用来分拣事件类型:
# 简化自 AgentScope 事件处理
for evt in stream:
match evt.type:
case "TEXT_BLOCK_DELTA":
text = evt.delta
render(text)
case "TOOL_CALL_START":
show_tool(evt.tool_call_name)
case "REPLY_END":
finalize()
case _:
pass # 忽略其他类型
这段代码不用一堆 if-elif,而是声明式地按事件类型分拣并提取字段。
陷阱
match/case 是 Python 3.10+ 语法。如果你面向 3.9 及以下,用 if/elif + isinstance() 替代。另外 case _ 通配符要放最后,否则后续分支永不执行。
三、参数解包:*args / **kwargs —— Java varargs 的推广
这是你写函数时会用到的语法,也是读懂框架源码的关键。
Java varargs vs Python *args
// Java varargs:接收不定个数的同类型参数
void log(String format, Object... args) { ... }
log("%s-%d", "user", 42);
# Python *args:接收不定个数的位置参数(打包成 tuple)
def log(format, *args):
for a in args:
...
return format, args
log("%s-%d", "user", 42) # args = ("user", 42)
**kwargs 收集关键字参数成 dict:
def connect(url, **kwargs):
# kwargs = {"timeout": 30, "retries": 3}
timeout = kwargs.get("timeout", 30)
为什么框架爱用它
框架用 *args/**kwargs 做透传(pass-through)——把参数原样传给内部实现,让上层 API 保持灵活:
# 简化自 AgentScope create_tools 模式
def create_tools(user_id="", agent_id="", **kwargs):
# kwargs 里可能是调用方额外传的扩展参数
...
**kwargs 让你"不收下所有参数也能安全接收未知参数"——这是框架 API 兼容性的常用手法。
四、关键字强制:def f(*, a) —— Java 无对应,Python 独有
这是 Java 开发者最容易困惑的语法。看 myagent 的真实源码:
# 源码: agentscope/app/_manager/_chat_run_registry.py
def spawn(self, coro, *, session_id, name=None):
...
* 后面紧跟的参数(session_id, name)是只允许按关键字传递的。
含义
def spawn(self, coro, *, session_id, name=None):
│ └───── * 之后的参数必须用关键字传
│
└── * 本身是"分隔符",不收集任何参数
# 合法
registry.spawn(coro, session_id="s1")
# 非法!session_id 必须按关键字传
# registry.spawn(coro, "s1") # TypeError
为什么这样设计
强制关键字参数让调用更自文档化——读代码的人一眼看出 session_id="s1" 是什么意思,而不会困惑于位置顺序。Java 没有对应物;最接近的是命名参数/构造器重载,但没有这种"强制关键字"的语法约束。
结合 *args 的完整签名
def f(*args, **kwargs): # 接收一切
def f(a, *, b): # a 位置传,b 必须关键字传
def f(a, b, *args): # a,b 位置,其余打包到 args
五、调用解包:f(*seq) / f(**d) —— Java 数组展开
上一章是"定义时接收",这一章是"调用时展开"。
Java 数组展开
Object[] args = {"user", 42};
String msg = String.format("%s-%d", args); // Java 会自动展开数组
Python 调用解包
# f(*seq) 把序列展开成位置参数
def add(a, b, c):
return a + b + c
nums = [1, 2, 3]
add(*nums) # == add(1, 2, 3),返回 6
# f(**d) 把 dict 展开成关键字参数
def connect(url, timeout=30, retries=3):
...
config = {"url": "x", "timeout": 10}
connect(**config) # == connect(url="x", timeout=10)
myagent 真实案例
ChatRunRegistry.spawn() 内部用 * 强制 + 解包,而调用方(ws.py)则用 create_task:
# 简化自 myagent ws.py
# 用 *tasks 展开多个 task 并行等待
done, pending = await asyncio.wait([task1, task2])
# 或
await asyncio.gather(*tasks) # 把 list 展开成多个参数
asyncio.gather(*tasks) 是典型用例——tasks 是一个 list,* 把它展开成 gather 的多个位置参数。Java 开发者会直觉想写 gather(tasks),但那样只传了一个参数(list),而 *tasks 才是正确的"展开"。
陷阱
* 展开的是可迭代对象,** 展开的是映射。展开 dict 时 key 必须是字符串(合法的参数名)。如果展开后和显式参数冲突,会报 TypeError。
六、推导式:list/dict/set —— Java Stream 的近亲
推导式(comprehension)是声明式地从一个可迭代对象生成另一个集合。
list 推导式
# 传统 for 循环
squares = []
for n in range(5):
squares.append(n * n)
# 推导式
squares = [n * n for n in range(5)] # [0, 1, 4, 9, 16]
# 带条件
evens = [n for n in range(10) if n % 2 == 0] # [0, 2, 4, 6, 8]
Java Stream 对比
// Java Stream
List<Integer> squares = IntStream.range(0, 5)
.map(n -> n * n)
.boxed()
.toList();
List<Integer> evens = IntStream.range(0, 10)
.filter(n -> n % 2 == 0)
.boxed()
.toList();
Python 推导式 Java Stream
────────────── ──────────────
[expr for x in xs] xs.stream().map(x -> expr)
[x for x in xs if c] xs.stream().filter(x -> c)
dict/set 推导式 Collectors.toMap/toSet
Python 推导式更紧凑,Java Stream 更显式地分步骤。两者都是"声明式变换"。
dict / set 推导式
# dict 推导式
squares_dict = {n: n*n for n in range(5)} # {0:0, 1:1, 2:4, ...}
# set 推导式
unique = {len(word) for word in ["hi", "hello", "hey"]} # {2, 3, 5}
陷阱
推导式适合"简单变换"。逻辑复杂(多步、异常处理)时,用普通循环更可读。嵌套推导式(for 多层的 [ ... for x in a for y in b ])会让可读性急剧下降——能不用就不用。
七、收尾:结构/模式语法是 Python 处理复杂数据的声明式原语
这一篇覆盖了 Python"结构/模式"语法家族:
结构/模式语法 作用 Java 锚点
───────────────────── ────────────── ──────────────
match/case 按结构分拣+解构 增强 switch
*args/**kwargs 接收不定参数 varargs
def f(*, a) 强制关键字参数 无对应
f(*seq)/f(**d) 调用时展开 数组展开
推导式 声明式集合变换 Stream
它们的共同点:让"处理复杂结构"更声明式、更紧凑。你在框架源码里看到的 *, **, match case, 推导式,本质都是这些原语在"分拣、展开、重组数据"。
读懂它们,你就能跨越"能写 Python"到"能读框架源码"的鸿沟——那些看似高深的结构处理代码,其实就这几板斧。
系列下一篇将深入类型系统(str | None、Self、泛型),继续解码"能读源码"所需的其他语法。想回顾语法基础,可读[现代 Python 语法解码系列首篇]的其余主题。
延伸思考:解包边界、match 性能、推导式可读性
f(*gen)展开一个生成器时,会先全部求值吗?对大数据的风险?match/case和if/elif在性能上有差别吗?什么场景值得用?- 嵌套推导式为什么难读?有没有保持声明式又可读的替代(如 generator 链)?
更多推荐


所有评论(0)