You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

关键改进点

  1. 避免安全风险:不再用eval执行整个异常构造节点,仅解析异常类的引用路径,大幅降低恶意代码执行的可能。
  2. 支持自定义异常:先执行函数内部的类定义(仅加载类,不运行函数逻辑),将自定义异常加载到局部命名空间后再解析。
  3. 覆盖reraise场景:通过AST父节点映射,定位raise所在的except块,收集该块捕获的异常类型(对应你例子中的ValueError)。
  4. 兼容多种引用方式:支持直接名称引用、带构造调用、跨模块属性引用等多种异常抛出写法。

注意事项

  • 对于运行时动态生成的异常(比如type(f"DynamicExc{id()}", (Exception,), {})),静态分析无法识别,因为这类异常是在函数运行时才创建的。
  • 对于跨模块引用的异常,需确保函数所在模块已正确导入对应模块(inspect获取的__globals__会包含函数所在模块的全局变量,一般可自动处理)。

备注:内容来源于stack exchange,提问作者DeszkaCodes

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 11:19:31