我们可以使用ast 模块解析和翻译我们的表达式。我们首先解析我们的语句,然后定义一个节点转换器,它将and 与&、or 与| 交换,并将变量名称包装在set 函数中。然后我们可以编译这个翻译后的 ast 并在我们的字典的上下文中对其进行评估。
import ast
from typing import Dict, Hashable, List, NoReturn, TypeVar
A = TypeVar('A', bound=Hashable)
def evaluate_logic(expr: str, context: Dict[str, List[A]]) -> List[A]:
tr = ast.parse(expr, mode='eval')
new_tr = ast.fix_missing_locations(TranslateLogic().visit(tr))
co = compile(new_tr, filename='', mode='eval')
return list(eval(co, context))
class TranslateLogic(ast.NodeTransformer):
def visit_BoolOp(self, node: ast.BoolOp) -> ast.BinOp:
op = node.op
new_op = ast.BitAnd() if isinstance(op, ast.And) else ast.BitOr()
return nested_op(new_op, [self.visit(value) for value in node.values])
def visit_Name(self, node: ast.Name) -> ast.Call:
return call_set(node)
def visit_Expression(self, node: ast.Expression) -> ast.Expression:
return super().generic_visit(node)
def generic_visit(self, node: ast.AST) -> NoReturn:
raise ValueError(f"cannote visit node: {node}")
def nested_op(op, values: List[ast.AST]) -> ast.BinOp:
if len(values) < 2:
raise ValueError(f"tried to nest operator with fewer than two values")
elif len(values) == 2:
left, right = values
return ast.BinOp(left=left, op=op, right=right)
else:
left, *rest = values
return ast.BinOp(left=left, op=op, right=nested_op(op, rest))
def call_set(node: ast.Name) -> ast.Call:
return ast.Call(func=ast.Name(id='set', ctx=node.ctx), args=[node], keywords=[])
if __name__ == '__main__':
expr = '(A and B) or C'
context = {'A': ['Hi', 'No', 'Yes'], 'B': ['Why', 'No', 'Okay'], 'C': ['Okay']}
print(evaluate_logic(expr, context))
# prints ['No', 'Okay']
我想说,这证明了在 Python 中进行通用解析以及在 Python 中应用自定义逻辑所面临的挑战,即使在利用现有解析库时也是如此。
一些笔记。我们最终会评估用户提供的代码。有一定的安全性,因为generic_visit 应该 如果用户提供比ands 和ors 更复杂的东西,但我会在生产情况下非常警惕这段代码。
其次,将and 转换为&(以及将or 转换为|)会有些复杂,因为Python 如何表示ands 链与&s 链。 ands 链成为具有多个值的单个 BoolOp 节点,而 & 链成为嵌套 BinOps 每个具有左和右的节点。比较
ast.dump(ast.parse('A and B and C', mode='eval'))
# "Expression(body=BoolOp(op=And(), values=[Name(id='A', ctx=Load()), Name(id='B', ctx=Load()), Name(id='C', ctx=Load())]))"
到
ast.dump(ast.parse('A & B & C', mode='eval'))
# "Expression(body=BinOp(left=BinOp(left=Name(id='A', ctx=Load()), op=BitAnd(), right=Name(id='B', ctx=Load())), op=BitAnd(), right=Name(id='C', ctx=Load())))"
这解释了为什么我们需要 nested_op 辅助函数。
最后,没有更多信息,我们无法实现not。原因是我们还没有定义“话语的宇宙”。特别是,not A 应该评估什么?我看到了两种可能的解决方案:
- 添加一个额外的参数来指定论域。添加
visit_UnaryOp 将not A 翻译成set(U) - set(A) 之类的东西,其中U 是话语的宇宙。
- 将
not 视为集差二元运算符。在这种情况下,将表达式预处理为字符串以将 " not " 替换为 " - " 可能是最简单的。
话虽如此,但如果您只是强迫您的用户使用(对您而言)更易于使用的界面,您可能会为自己省去很多麻烦。类似的东西
from my_module import And, Or
expr = Or(And("A", "B"), "C")
context = {'A': ['Hi', 'No', 'Yes'], 'B': ['Why', 'No', 'Okay'], 'C': ['Okay']}
evaluate_logic(expr, context)
你强迫你的用户预先解析他们给你的表达式,但你可以省去很多担心和麻烦。