本文面向有 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/casePython 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 | NoneSelf、泛型),继续解码"能读源码"所需的其他语法。想回顾语法基础,可读[现代 Python 语法解码系列首篇]的其余主题。

延伸思考:解包边界、match 性能、推导式可读性

  • f(*gen) 展开一个生成器时,会先全部求值吗?对大数据的风险?
  • match/caseif/elif 在性能上有差别吗?什么场景值得用?
  • 嵌套推导式为什么难读?有没有保持声明式又可读的替代(如 generator 链)?
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐