python 计算已解析的逻辑表达式(没有 eval)

python calculating parsed logical expression (without eval)

我正在寻找计算包含逻辑表达式的字符串表达式的最佳方法,例如:x and not y or (z or w) 或 json 逻辑,例如:{"and": {"x": "true", "y": "false"}, "or": {"or": {"z": "true", "w": "True"}}}

我决定配置文件应该是什么样子,这样格式就可以改变,这只是一个例子。

我看到了一种使用 pyparse 将第一个字符串解析为类似以下内容的方法:['x', 'AND', 'y', 'OR', ['z', 'OR', 'W']] 但我不知道如何在解析后获取表达式的值。

关于 json 逻辑格式 我看到包 json-logic 试过了,但似乎代码被破坏了,因为我不能 运行 他们的任何例子所以如果有是我认为会很棒的其他东西。

x = True
y = False
z = False
w = False
calc_string_as_bool('x and not y or (z or w)') -> True

感谢您的帮助。

在我看来你想要的东西 eval,即使你说没有 eval。

In [1]: True and True
Out[1]: True

In [2]: e = "True and True"

In [3]: eval(e)
Out[3]: True

In [4]: el = ["True", "and", "True"]

In [5]: eval(" ".join(el))
Out[5]: True

也许您可以澄清为什么不能选择使用 eval 来执行 eval 的操作。


添加示例,基于相关示例:

def eval_recurse(ops, replacements):
    evaluated = []
    for op in ops:
        if type(op) is list:
            evaluated.append(str(eval_recurse(op, replacements)))
            continue
        if str(op).lower() in ["and", "not", "or"]:
            op = op.lower()
        if op in replacements:
            op = str(replacements[op])
        evaluated.append(op)
    return eval(" ".join(evaluated))


if __name__ == "__main__":
    replacements = {"x": True, "y": False, "z": False, "w": False}
    print(eval_recurse(["x", "AND", "NOT", "y", "OR", ["z", "OR", "w"]], replacements))

这会产生 True。这可能没有帮助,因为您不想使用 eval,但我想我会提供它以防万一。

如果您想使用 Python 的解析器而不是它的求值器,您可以这样做。您必须自己评估表达式,但如果您只需要处理布尔表达式,那不会太困难。无论如何,这是学习如何编写求值器而不必担心解析的合理方法。

要解析表达式,可以使用 ast.parsemode='eval';这将 return 一个没有评估任何东西的 AST。

评估 AST 通常是通过 depth-first 扫描完成的,这很容易编写,但是 ast 模块带有 NodeVisitor class 来处理一些细节。您需要创建自己的 NodeVisitor 的子 class,在其中为每个 AST 节点类型 X 定义名为 visit_X 的方法。这些方法可以 return 结果,因此通常是评估器将为可以评估的特定 AST 节点类型定义访问者方法;这些方法将使用 subclass 的 visit 方法评估每个子节点,适当地组合值,以及 return 结果。

这可能不是很清楚,所以我将在下面举一个例子。

重要提示:AST 节点都记录在 Python's library reference manual 中。在编写评估程序时,您需要经常参考该文档。确保您使用的文档版本与您的 Python 版本相对应,因为 AST 结构确实会不时更改。

这是一个简单的求值器(老实说!主要是注释),它处理布尔运算符 andifnot。它通过在字典中查找变量的名称来处理变量(在构造求值器对象时必须提供字典),并且它知道常量值。它不理解的任何内容都会导致它引发异常,而我尽量做到相当严格。

import ast
class BooleanEvaluator(ast.NodeVisitor):
    def __init__(self, symtab = None):
        '''Create a new Boolean Evaluator.
           If you want to allow named variables, give the constructor a
           dictionary which maps names to values. Any name not in the 
           dictionary will provoke a NameError exception, but you could
           use a defaultdict to provide a default value (probably False).
           You can modify the symbol table after the evaluator is
           constructed, if you want to evaluate an expression with different
           values.
        '''
        self.symtab = {} if symtab is None else symtab

    # Expression is the top-level AST node if you specify mode='eval'.
    # That's not made very clear in the documentation. It's different
    # from an Expr node, which represents an expression statement (and
    # there are no statements in a tree produced with mode='eval').
    def visit_Expression(self, node):
        return self.visit(node.body)

    # 'and' and 'or' are BoolOp, and the parser collapses a sequence of
    # the same operator into a single AST node. The class of the .op
    # member identifies the operator, and the .values member is a list 
    # of expressions.
    def visit_BoolOp(self, node):
        if isinstance(node.op, ast.And):
            return all(self.visit(c) for c in node.values)
        elif isinstance(node.op, ast.Or):
            return any(self.visit(c) for c in node.values)
        else:
            # This "shouldn't happen".
            raise NotImplementedError(node.op.__doc__ + " Operator")

    # 'not' is a UnaryOp. So are a number of other things, like unary '-'.
    def visit_UnaryOp(self, node):
        if isinstance(node.op, ast.Not):
             return not self.visit(node.operand)
        else:
            # This error can happen. Try using the `~` operator.
            raise NotImplementedError(node.op.__doc__ + " Operator")

    # Name is a variable name. Technically, we probably should check the
    # ctx member, but unless you decide to handle the walrus operator you
    # should never see anything other than `ast.Load()` as ctx.
    # I didn't check that the symbol table contains a boolean value,
    # but you could certainly do that.
    def visit_Name(self, node):
        try:
            return self.symtab[node.id]
        except KeyError:
            raise NameError(node.id)

    # The only constants we're really interested in are True and False,
    # but you could extend this to handle other values like 0 and 1
    # if you wanted to be more liberal
    def visit_Constant(self, node):
        if isinstance(node.value, bool):
            return node.value
        else:
            # I avoid calling str on the value in case that executes
            # a dunder method.
            raise ValueError("non-boolean value")

    # The `generic_visit` method is called for any AST node for
    # which a specific visitor method hasn't been provided.
    def generic_visit(self, node):
        raise RuntimeError("non-boolean expression")

下面是如何使用评估器的示例。此函数采用恰好使用三个变量(必须命名为 x、y 和 z)的表达式,并生成真值 table.

import itertools
def truthTable(expr):
    evaluator = BooleanEvaluator()
    theAST = ast.parse(expr, mode='eval')
    print("x  y  z  " + expr) 
    for x, y, z in itertools.product([True, False], repeat=3):
        evaluator.symtab = {'x': x, 'y': y, 'z': z}
        print('  '.join("FT"[b] for b in (x, y, z, evaluator.visit(theAST))))

truthTable('x and (y or not z) and (not y or z)')