如何使用Python的AST检测内置函数、类名被意外赋值的Bug
用AST检测内置名被覆盖Bug的实现方案
对应报错场景复现:
>>> list("abc") ['a', 'b', 'c'] >>> list = 42 >>> list("xyz") Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'int' object is not callable
这类给内置类、内置函数赋值导致原有定义被覆盖的问题,完全可以用Python标准库的ast模块做静态扫描检测,不需要运行代码就能提前发现问题,实现逻辑非常直接:
- 先拿到所有内置名称的基线名单
不用手动整理内置函数、内置类、内置异常的名称,直接从builtins模块取就行,自动适配不同Python版本的内置名差异:import builtins # 收集所有内置名称作为检测黑名单 BUILTIN_BLACKLIST = set(dir(builtins)) - 解析源码生成AST后,遍历所有会产生名称绑定的节点
要覆盖所有会把名称绑定到当前作用域的语法场景,漏了就会出现检测不到的情况:- 普通赋值语句:对应AST节点
ast.Assign,比如list = 42这类直接赋值的场景,提取赋值目标的变量名 - 函数/类定义:对应
ast.FunctionDef、ast.AsyncFunctionDef、ast.ClassDef节点,比如自定义函数叫eval、自定义类叫dict的场景 - 导入语句:对应
ast.Import、ast.ImportFrom节点,比如from os import open这种导入语句覆盖内置open的场景,要同时处理别名的情况
- 普通赋值语句:对应AST节点
- 命中黑名单直接告警
把提取到的每个待绑定名称和内置黑名单做匹配,只要命中就记录告警信息,标注清楚问题所在的行号、被覆盖的内置名即可。 - (可选优化)按作用域降低误报
如果觉得检测太严格,可以区分作用域:全局作用域下覆盖内置名的影响范围最大,优先告警;函数/类内部的局部作用域如果定义了和内置名同名的变量,只要作用域内没有把该名称当原有内置功能调用,影响范围有限,可以调整告警级别。
下面是可以直接跑的最简检测脚本:
import ast import builtins BUILTIN_BLACKLIST = set(dir(builtins)) class BuiltinOverwriteChecker(ast.NodeVisitor): def __init__(self): self.alerts = [] def _match_builtin(self, name, line_no): if name in BUILTIN_BLACKLIST: self.alerts.append(f"Line {line_no}: 内置名称 '{name}' 被覆盖,可能触发'int object is not callable'这类运行时报错") # 检测普通赋值 def visit_Assign(self, node): for target in node.targets: if isinstance(target, ast.Name): self._match_builtin(target.id, target.lineno) self.generic_visit(node) # 检测函数/异步函数定义 def visit_FunctionDef(self, node): self._match_builtin(node.name, node.lineno) self.generic_visit(node) def visit_AsyncFunctionDef(self, node): self._match_builtin(node.name, node.lineno) self.generic_visit(node) # 检测类定义 def visit_ClassDef(self, node): self._match_builtin(node.name, node.lineno) self.generic_visit(node) # 检测import导入 def visit_Import(self, node): for alias in node.names: bind_name = alias.asname if alias.asname else alias.name.split('.')[0] self._match_builtin(bind_name, node.lineno) self.generic_visit(node) def visit_ImportFrom(self, node): for alias in node.names: bind_name = alias.asname if alias.asname else alias.name self._match_builtin(bind_name, node.lineno) self.generic_visit(node) if __name__ == "__main__": # 测试用的问题代码 test_src = """ list("abc") list = 42 list("xyz") """ ast_tree = ast.parse(test_src) checker = BuiltinOverwriteChecker() checker.visit(ast_tree) for alert in checker.alerts: print(alert)
运行上面的脚本,会直接输出Line 3: 内置名称 'list' 被覆盖,可能触发'int object is not callable'这类运行时报错,不需要运行代码就能精准定位问题位置。
内容的提问来源于stack exchange,提问作者Ummulkhair Kadri
相关产品推荐
相关产品推荐

