Python如何不使用eval将类实例化字符串转为实际函数调用
无eval实现类实例字符串解析的方案
哪怕你完全信任数据库写入的内容,也可以通过AST解析+标准库动态导入的方式实现需求,全程避免执行任意动态代码,安全性和可控性远高于eval/exec。
核心实现逻辑分5步:
- 用
ast.parse将字符串解析为语法树,强制校验语句格式必须为合法的可调用对象调用,从根源拦截非预期语法 - 拆分调用链,区分模块路径与目标类/可调用对象名称
- 用
importlib.import_module替代exec实现模块动态导入,通过getattr逐层获取目标类 - 用
ast.literal_eval解析构造参数,该方法仅能解析字符串、数字、布尔、列表、字典等Python字面量,无法执行函数调用或其他恶意逻辑 - 传入解析完成的参数完成实例化
完整实现代码如下:
import ast import importlib def safe_create_instance(statement: str, base_module: str = "parent.module"): # 解析AST,仅允许单一的调用表达式 tree = ast.parse(statement, mode="eval") call_node = tree.body if not isinstance(call_node, ast.Call): raise ValueError(f"非法的实例化语句: {statement}, 仅允许类/函数调用格式") # 拆分完整调用属性链 attr_chain = [] current = call_node.func while isinstance(current, ast.Attribute): attr_chain.append(current.attr) current = current.value if not isinstance(current, ast.Name): raise ValueError(f"非法的调用目标格式: {statement}") attr_chain.append(current.id) attr_chain.reverse() if len(attr_chain) < 2: raise ValueError(f"调用目标必须包含模块路径: {statement}") # 动态导入模块,优先从指定的base_module下查找,兼容原有导入逻辑 target = None remaining_attrs = [] # 从长到短尝试匹配可导入的模块路径 for split_idx in range(len(attr_chain), 0, -1): try: mod_candidate = ".".join([base_module] + attr_chain[:split_idx]) target = importlib.import_module(mod_candidate) remaining_attrs = attr_chain[split_idx:] break except ImportError: continue # 基础路径下找不到时尝试绝对导入 if target is None: for split_idx in range(len(attr_chain), 0, -1): try: mod_candidate = ".".join(attr_chain[:split_idx]) target = importlib.import_module(mod_candidate) remaining_attrs = attr_chain[split_idx:] break except ImportError: continue if target is None: raise ImportError(f"无法导入语句对应的模块: {statement}") # 顺着属性链拿到最终要实例化的类/可调用对象 for attr in remaining_attrs: target = getattr(target, attr) # 解析构造参数,仅支持字面量值 args = [] for arg_node in call_node.args: try: args.append(ast.literal_eval(arg_node)) except ValueError: raise ValueError(f"构造参数仅支持字面量值,非法参数位置: {ast.dump(arg_node)}") kwargs = {} for kw_node in call_node.keywords: try: kwargs[kw_node.arg] = ast.literal_eval(kw_node.value) except ValueError: raise ValueError(f"构造参数仅支持字面量值,非法参数{kw_node.arg}: {ast.dump(kw_node)}") # 完成实例化 return target(*args, **kwargs) # 调用示例 statement = r'module1.CustomHandler(filename="abc.csv", mode="rb")' instance = safe_create_instance(statement)
注意:如果需要支持非字面量参数(如枚举、预定义常量),请在参数解析环节增加严格白名单,仅放行可信的可调用对象/常量,不要放开任意代码执行权限。
如果你的业务场景格式非常固定,也可以通过正则拆分字符串实现,但AST方案的鲁棒性更强,不会因为参数值包含括号、点号等特殊字符出现解析错误。
内容的提问来源于stack exchange,提问作者Foocli
相关产品推荐
相关产品推荐

