"AST 断言重写"
你平时写 assert result == expected,在 Pytest 底层,这行代码在运行前就被语法树(AST)重写了。
Pytest 把它变成了一段极其复杂的代码,不仅判断结果对不对,还会把 result 和 expected 的每一个属性、每一个变量的值都打印出来。这就是为什么 Pytest 报错时,能给你提供那么丰富、那么详细的上下文信息。
一个极其重要的"避坑"指南
既然你学到了这里,我必须告诉你一个 AST 重写机制的致命弱点:
如果你在 assert 后面加了自定义的提示信息,AST 重写就会立刻失效!
python
# ❌ 错误写法:加了逗号后面的提示信息,Pytest 的丰富报错就没了!
assert a == b, "a 和 b 不相等"
# ✅ 正确写法:让 Pytest 尽情发挥 AST 重写的能力
assert a == b
为什么? 因为 Pytest 的 AST 重写机制在解析语法树时,如果发现 assert 节点带了自定义的 Message 参数,为了尊重开发者的原始意图,它会放弃重写,退化为原生 Python 的 raise AssertionError("a 和 b 不相等")。
真正想问的底层逻辑是:Pytest 的底层源码,究竟是如何"看懂"并识别出 assert a == b, "提示信息" 这种带逗号写法的?
这就要回到我们上一轮聊到的核心概念:AST(抽象语法树)。
在 Python 的 AST 语法树眼里,assert 语句并不是简单的文本,而是一个结构化的"节点(Node)"。这个节点有两个非常关键的属性(插槽):
test:存放断言的条件(比如 a == b)。
msg:存放自定义的提示信息(比如 "a 和 b 不相等")。
当你在代码里写下这两种不同的写法时,Python 编译器生成的 AST 节点是完全不同的:
写法一:不带提示信息
python
assert a == b
生成的 AST 节点:
test = a == b
msg = None (空的)
写法二:带了提示信息(有逗号)
python
assert a == b, "a 和 b 不相等"
生成的 AST 节点:
test = a == b
msg = "a 和 b 不相等" (有具体的字符串值)
Pytest 源码是如何"看懂"的?
Pytest 在重写代码时,会在源码里写一个极其明确的 if...else... 判断逻辑(你可以去 Pytest 源码 src/_pytest/assertion/rewrite.py 的 visit_Assert 方法里亲眼看到它)。
它的底层伪代码逻辑大概是这样的:
python
def visit_Assert(self, node):
# Pytest 在这里"看" AST 节点的 msg 属性
if node.msg is not None:
# 发现 msg 不是空的!说明用户自己写了自定义提示信息。
# 策略:放弃重写,直接保留原样,退化为原生 Python 的 assert 行为。
return node
else:
# 发现 msg 是空的!说明用户只写了 assert a == b。
# 策略:启动 AST 魔法重写,把这段代码变成包含丰富调试信息的复杂代码!
return self.rewrite_assert(node)
总结
所以,计算机(Pytest)并不是像人类一样去"阅读"你的注释或代码习惯。它是通过解析 AST 语法树的结构,极其精确地判断 msg 这个属性到底有没有值。
如果 msg 有值,它就认为:"哦,用户有自己的报错需求,我不插手。"
如果 msg 没值,它就认为:"太好了,用户需要我的高级调试功能,我来重写它!"