使用Python AST获取多行import语句起始行号的实现方法
问题描述
在Python中通过ast模块定位所有ast.Import与ast.ImportFrom节点时,处理如下多行导入语句会遇到行号获取偏差问题:
from foo import ( bar, baz, coo )
上述场景下,node.names列表里每个导入项(bar、baz、coo)的lineno属性返回的是对应导入项自身所在行的行号,而非from foo import所在的导入语句起始行号。需要实现两个需求:
- 正确获取导入语句的起始行号(单行导入场景下该值和语句行号一致)
- 实现可迭代遍历脚本内所有作用域、获取各作用域下全部导入语句的方法
更新说明:后续排查发现
ast.walk本身不会按行序返回节点,问题根源是之前错误地为node.names中的每个元素都生成了独立记录元组,修正逻辑为仅关联导入节点自身、使用导入节点的lineno属性即可解决行号问题,因相关回答仍有参考价值故保留问题。
解决方案
1. 导入语句起始行号获取方法
不要读取node.names列表中单个alias导入项对象的lineno属性,直接取ast.Import/ast.ImportFrom节点自身的lineno属性即可。这个属性固定返回整个导入语句的起始行位置,不受导入换行、括号包裹写法的影响,单行导入场景下也和预期行号完全匹配。
import ast code = """ from foo import ( bar, baz, coo ) import os """ tree = ast.parse(code) for node in ast.walk(tree): if isinstance(node, (ast.Import, ast.ImportFrom)): print(f"导入语句起始行号: {node.lineno}") # 多行导入输出行号为2,与from foo import所在行完全一致
2. 全作用域导入语句遍历方法
ast.walk是扁平化遍历逻辑,不会保留节点的作用域归属关系,如果需要按作用域归集导入语句,可以自定义递归遍历逻辑:在遇到函数定义、异步函数定义、类定义这类生成新作用域的节点时,更新当前作用域标记,递归进入其作用域块收集导入即可。
from collections import defaultdict def collect_imports_by_scope(node, scope_path="__main__", result=None): if result is None: result = defaultdict(list) # 遍历当前节点下的直接子节点 for child in ast.iter_child_nodes(node): if isinstance(child, (ast.Import, ast.ImportFrom)): result[scope_path].append({ "lineno": child.lineno, "node": child }) # 识别生成新作用域的节点,切换作用域路径后递归 elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): new_scope = f"{scope_path}.{child.name}" collect_imports_by_scope(child, new_scope, result) # 非作用域节点保持当前作用域路径继续遍历 else: collect_imports_by_scope(child, scope_path, result) return result # 调用示例 imports_by_scope = collect_imports_by_scope(tree) for scope, imports in imports_by_scope.items(): print(f"作用域 {scope} 下的导入数量: {len(imports)}")
内容的提问来源于stack exchange,提问作者DrownedSuccess
相关产品推荐
相关产品推荐

