如何在Python中识别raise语句显式抛出的所有异常类型?
如何在Python中识别raise语句显式抛出的所有异常类型?
问题背景
你正在开发一个源码分析工具,需要收集目标函数中显式通过raise关键字抛出的所有异常类型。比如下面这个函数:
from some.package import CustomError class NewException(Exception):... def something(i): if i == 2: raise RuntimeError("This is a runtime error") else: raise CustomError("This is a custom error") try: something() except ValueError: raise except Exception as e: raise NewException(e) from e
你期望得到的结果是集合:{<class RuntimeError>, <class some.package.CustomError>, <class ValueError>, <class [function_path].NewException>}。
你目前尝试用ast和inspect模块解析函数源码生成AST,但卡在了自定义异常的识别上——直接用eval解析AST节点时,程序会崩溃,而且这种方式还存在执行恶意代码的安全风险。
解决思路:安全解析AST,避免直接执行代码
核心问题出在eval的使用上:它会实际执行代码,既不安全,也无法处理未在当前上下文定义的自定义异常。我们可以换一种思路:通过AST分析异常的引用路径,结合函数的全局/局部命名空间来安全地解析异常类,而不是直接执行节点代码。
下面是改进后的实现方案:
import inspect import ast from typing import Callable, Any, Set, Optional from types import FunctionType def _get_exception_class_from_node(node: ast.expr, global_ns: dict, local_ns: dict) -> Optional[type[BaseException]]: """从AST节点中安全解析出异常类,不执行任意风险代码""" # 处理直接名称引用:比如 raise ValueError if isinstance(node, ast.Name): try: cls = eval(ast.unparse(node), global_ns, local_ns) except (NameError, TypeError): return None if isinstance(cls, type) and issubclass(cls, BaseException): return cls # 处理带构造调用的异常:比如 raise RuntimeError("msg") elif isinstance(node, ast.Call) and isinstance(node.func, (ast.Name, ast.Attribute)): try: cls = eval(ast.unparse(node.func), global_ns, local_ns) except (NameError, TypeError): return None if isinstance(cls, type) and issubclass(cls, BaseException): return cls # 处理跨模块/嵌套属性引用:比如 raise some.package.CustomError elif isinstance(node, ast.Attribute): try: cls = eval(ast.unparse(node), global_ns, local_ns) except (NameError, TypeError): return None if isinstance(cls, type) and issubclass(cls, BaseException): return cls return None def _build_parent_map(tree: ast.AST) -> dict[ast.AST, ast.AST]: """为AST节点建立父节点映射,用于定位reraise所在的except块""" parent_map = {} for node in ast.walk(tree): for child in ast.iter_child_nodes(node): parent_map[child] = node return parent_map def _find_parent_except_node(node: ast.Raise, parent_map: dict[ast.AST, ast.AST]) -> Optional[ast.ExceptHandler]: """找到当前reraise节点对应的最近except块""" current_node = node while current_node in parent_map: current_node = parent_map[current_node] if isinstance(current_node, ast.ExceptHandler): return current_node return None def analyze_function(function: Callable[..., Any]) -> Set[type[BaseException]]: exceptions: Set[type[BaseException]] = set() # 跳过非函数对象 if not isinstance(function, FunctionType): return exceptions # 获取函数源码与命名空间 try: source = inspect.getsource(function) except OSError: print("无法获取函数源码,可能是内置函数或动态生成的函数") return exceptions global_ns = function.__globals__ local_ns = {} tree = ast.parse(source, mode='exec') parent_map = _build_parent_map(tree) # 先加载函数内部定义的自定义异常类 for stmt in tree.body: if isinstance(stmt, ast.ClassDef): try: exec(ast.unparse(stmt), global_ns, local_ns) except Exception as e: print(f"加载内部类定义失败: {e}") # 遍历所有raise节点 for node in ast.walk(tree): if not isinstance(node, ast.Raise): continue # 处理无明确异常的reraise(比如except块中的raise) if node.exc is None: except_node = _find_parent_except_node(node, parent_map) if except_node and except_node.type is not None: # 处理except块的多异常情况,比如 except (ValueError, TypeError): for exc_node in (except_node.type if isinstance(except_node.type, ast.Tuple) else [except_node.type]): exc_cls = _get_exception_class_from_node(exc_node, global_ns, local_ns) if exc_cls: exceptions.add(exc_cls) continue # 处理有明确异常的raise语句 exc_cls = _get_exception_class_from_node(node.exc, global_ns, local_ns) if exc_cls: exceptions.add(exc_cls) return exceptions
关键改进点
- 避免安全风险:不再用
eval执行整个异常构造节点,仅解析异常类的引用路径,大幅降低恶意代码执行的可能。 - 支持自定义异常:先执行函数内部的类定义(仅加载类,不运行函数逻辑),将自定义异常加载到局部命名空间后再解析。
- 覆盖reraise场景:通过AST父节点映射,定位
raise所在的except块,收集该块捕获的异常类型(对应你例子中的ValueError)。 - 兼容多种引用方式:支持直接名称引用、带构造调用、跨模块属性引用等多种异常抛出写法。
注意事项
- 对于运行时动态生成的异常(比如
type(f"DynamicExc{id()}", (Exception,), {})),静态分析无法识别,因为这类异常是在函数运行时才创建的。 - 对于跨模块引用的异常,需确保函数所在模块已正确导入对应模块(
inspect获取的__globals__会包含函数所在模块的全局变量,一般可自动处理)。
备注:内容来源于stack exchange,提问作者DeszkaCodes
相关产品推荐
相关产品推荐

