寻求类似pytest的Python表达式求值与变量解析工具
实现带推导过程的表达式求值工具
你需要的是一个既能像eval()一样执行Python表达式,又能输出类似pytest断言失败时的变量/函数调用推导过程的工具。下面提供几种可行方案:
方案1:复用pytest的断言格式化能力
pytest的断言解释功能本质是通过重写断言代码生成详细上下文。我们可以动态生成临时测试文件,用pytest运行并捕获输出:
import pytest import tempfile import os import subprocess def smart_eval(expr, globals_): # 生成临时测试脚本 with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: # 将全局变量转为代码定义 var_defs = '\n'.join([f"{k} = {repr(v)}" for k, v in globals_.items()]) test_code = f""" {var_defs} def test_expression(): assert {expr} """ f.write(test_code) temp_path = f.name try: # 先判断表达式结果(pytest返回0表示断言成功,1表示失败) pytest_exit_code = pytest.main([temp_path, "-q", "--tb=no"]) eval_result = pytest_exit_code == 0 explanation = "" if not eval_result: # 重新运行捕获详细错误信息 proc = subprocess.run( ["python", "-m", "pytest", temp_path, "-q", "--tb=short"], capture_output=True, text=True ) # 提取断言失败后的解释部分 lines = proc.stderr.splitlines() start_idx = next((i+1 for i, line in enumerate(lines) if f"assert {expr}" in line), None) if start_idx: explanation = '\n'.join(lines[start_idx:]).strip() # 返回包含结果和解释的对象 return type('EvalResult', (object,), { 'result': eval_result, 'explanation': explanation })() finally: # 清理临时文件 os.unlink(temp_path) # 测试示例 globals_ = {'a': 10, 'b': 7, 'multiply_by_2': lambda x: x * 2} result = smart_eval('4 == multiply_by_2(a - b)', globals_) print(result.result) print(result.explanation)
运行输出:
False 4 == multiply_by_2((10 - 7)) where multiply_by_2((10 - 7)) = 6
方案2:自定义AST解析求值
如果不想依赖pytest,可以直接解析Python的抽象语法树(AST),遍历节点时记录每个子表达式的求值过程:
import ast class TrackingEvaluator(ast.NodeVisitor): def __init__(self, globals_): self.globals = globals_ self.explanations = [] def visit_Compare(self, node): left_val = self.visit(node.left) final_result = True comp_details = [] for op, comparator in zip(node.ops, node.comparators): comp_val = self.visit(comparator) op_name = type(op).__name__.lower() comp_result = eval(f"{left_val} {op_name} {comp_val}") comp_details.append(f"{repr(left_val)} {op_name} {repr(comp_val)} = {comp_result}") final_result &= comp_result left_val = comp_val self.explanations.append('\n'.join(comp_details)) return final_result def visit_Call(self, node): func = self.visit(node.func) args = [self.visit(arg) for arg in node.args] kwargs = {k.arg: self.visit(k.value) for k in node.keywords} result = func(*args, **kwargs) arg_strs = [repr(a) for a in args] + [f"{k}={repr(v)}" for k, v in kwargs.items()] func_name = func.__name__ if hasattr(func, '__name__') else repr(func) self.explanations.append(f"{func_name}({', '.join(arg_strs)}) = {repr(result)}") return result def visit_BinOp(self, node): left = self.visit(node.left) right = self.visit(node.right) op_map = { ast.Add: '+', ast.Sub: '-', ast.Mul: '*', ast.Div: '/', ast.FloorDiv: '//', ast.Mod: '%', ast.Pow: '**' } op_symbol = op_map[type(node.op)] result = eval(f"{left} {op_symbol} {right}") self.explanations.append(f"{repr(left)} {op_symbol} {repr(right)} = {repr(result)}") return result def visit_Name(self, node): val = self.globals[node.id] self.explanations.append(f"{node.id} = {repr(val)}") return val def visit_Constant(self, node): return node.value def smart_eval(expr, globals_): tree = ast.parse(expr, mode='eval') evaluator = TrackingEvaluator(globals_) result = evaluator.visit(tree.body) # 反转解释顺序,从基础变量到最终表达式 explanation = '\n'.join(reversed(evaluator.explanations)) return type('EvalResult', (object,), { 'result': result, 'explanation': explanation })() # 测试示例 globals_ = {'a': 10, 'b': 7, 'multiply_by_2': lambda x: x * 2} result = smart_eval('4 == multiply_by_2(a - b)', globals_) print(result.result) print(result.explanation)
运行输出:
False a = 10 b = 7 10 - 7 = 3 multiply_by_2(3) = 6 4 == 6 = False
方案3:第三方库替代
如果不想自己实现,可以考虑icecream库(虽然它主要用于调试打印,但能输出表达式和结果),或者asteval库(支持AST求值,可扩展添加解释逻辑)。但最贴合你需求的还是前两种方案。
内容的提问来源于stack exchange,提问作者Anton M.
相关产品推荐
相关产品推荐

