在Python中将布尔表达式评估为字符串的更快方法

时间:2013-12-23 08:14:20

标签: python string boolean expression eval

我现在已经在这个项目上工作了几个月。该项目的最终目标是评估类似于功能测试的整个数字逻辑电路;只是为了给出一个问题的范围。我在这里创建的主题处理了我在分析布尔表达式时遇到的问题。对于数字电路内的任何门,它具有全局输入的输出表达式。 EX:((A&B)|(C&D)^E)。然后我想用这个表达式来计算所有可能的结果,并确定每个输入对结果的影响程度。

我发现的最快的方法是将真值表构建为矩阵并查看某些行(不会因为它是offtopic而进入该算法的细节),问题是曾经是唯一输入的数量超过26-27(大约是这个)内存使用量远远超过16GB(我的电脑最大)。您可能会说“购买更多内存”,但随着输入的每次增加1,内存使用量增加一倍。我分析的一些表达式超过200个独特的输入......

我现在使用的方法使用compile方法将表达式作为字符串。然后我创建一个数组,其中包含从compile方法中找到的所有输入。然后我从可能值的样本中随机选择“True”和“False”行生成一个列表(如果样本大小与范围大小相同,那么它将等同于真值表中的行当事情变得太长而无法计算时,我会限制样本量。然后使用输入名称压缩这些值并用于评估表达式。这将给出初始结果,之后我在随机布尔列表中逐列,然后翻转布尔值,然后再次使用输入拉链并再次评估它以确定结果是否更改。

所以我的问题是:有更快的方法吗?我已经包含了执行工作的代码。我已经尝试过正则表达式来查找和替换,但它总是较慢(从我所见过的)。考虑到内部for循环将运行 N 次,其中 N 是唯一输入的数量。如果 N >,则外部for循环I限制为运行2 ^ 15 15.所以这变成了eval被执行Min(2^N, 2^15) * (1 + N) ...

作为更新来澄清我的确切要求(抱歉任何混淆)。用于计算我需要的算法/逻辑不是问题。我要求替代python内置'eval',它将更快地执行相同的操作。 (以布尔表达式的格式取一个字符串,用字典中的值替换字符串中的变量,然后计算字符串)。

#value is expression as string
comp = compile(value.strip(), '-', 'eval')
inputs = comp.co_names
control = [0]*len(inputs)

#Sequences of random boolean values to be used
random_list = gen_rand_bits(len(inputs))


for row in random_list:
    valuedict = dict(zip(inputs, row))
    answer = eval(comp, valuedict)

    for column in range(len(row)):
        row[column] = ~row[column]

        newvaluedict = dict(zip(inputs, row))
        newanswer = eval(comp, newvaluedict)

        row[column] = ~row[column]

        if answer != newanswer:
            control[column] = control[column] + 1

3 个答案:

答案 0 :(得分:4)

我的问题:

  

只是为了确保我理解正确:你的实际问题是确定布尔表达式中每个变量对所述表达式结果的相对影响?

OP回答:

  

这就是我在计算的但我的问题不在于我如何逻辑地计算它,而是使用内置的python eval来执行评估。


所以,这似乎是一个经典的XY problem。您有一个实际问题是确定每个变量在布尔表达式中的相对影响。您试图以相当低效的方式解决这个问题,现在您实际上“感觉”效率低下(在内存使用和运行时间方面),您寻找改进解决方案的方法,而不是寻找更好的方法来解决您的原始问题问题

无论如何,让我们先来看看你是如何解决这个问题的。我不确定gen_rand_bits应该做什么,所以我不能真正考虑到这一点。但是,您实际上是在尝试变量赋值的所有可能组合,并查看是否翻转单个变量的值会更改公式结果的结果。 “Luckily”,这些只是布尔变量,因此您“仅”查看2^N种可能的组合。这意味着您具有指数运行时间。现在,O(2^N)算法在理论上非常非常糟糕,而在实践中使用它们通常有点好(因为大多数算法具有可接受的平均情况并且执行得足够快)。但是,作为一个详尽的算法,您实际上必须查看每个组合而不能快捷方式。另外,使用Python eval进行编译和价值评估显然不会让低效算法接受。

因此,我们应该寻找不同的解决方案。在查看您的解决方案时,可能会说效率更高是不可能的,但在查看原始问题时,我们可以反驳。

你基本上想要做与static analysis类似的编译器。您希望查看源代码并从那里进行分析,而无需实际评估。由于您正在分析的语言受到严格限制(只是一个布尔表达式,只有很少的运算符),这并不是那么难。

代码分析通常适用于abstract syntax tree(或其增强版本)。 Python使用ast模块提供代码分析和抽象语法树生成。我们可以使用它来解析表达式并获得AST。然后,基于树,我们可以分析表达式的每个部分与整体的相关性。

现在,评估每个变量的相关性会变得非常复杂,但您可以通过分析语法树来完成所有这些操作。我将向您展示一个支持所有布尔运算符的简单评估,但不会进一步检查表达式的语义影响:

import ast

class ExpressionEvaluator:
    def __init__ (self, rawExpression):
        self.raw = rawExpression
        self.ast = ast.parse(rawExpression)

    def run (self):
        return self.evaluate(self.ast.body[0])

    def evaluate (self, expr):
        if isinstance(expr, ast.Expr):
            return self.evaluate(expr.value)
        elif isinstance(expr, ast.Name):
            return self.evaluateName(expr)
        elif isinstance(expr, ast.UnaryOp):
            if isinstance(expr.op, ast.Invert):
                return self.evaluateInvert(expr)
            else:
                raise Exception('Unknown unary operation {}'.format(expr.op))
        elif isinstance(expr, ast.BinOp):
            if isinstance(expr.op, ast.BitOr):
                return self.evaluateBitOr(expr.left, expr.right)
            elif isinstance(expr.op, ast.BitAnd):
                return self.evaluateBitAnd(expr.left, expr.right)
            elif isinstance(expr.op, ast.BitXor):
                return self.evaluateBitXor(expr.left, expr.right)
            else:
                raise Exception('Unknown binary operation {}'.format(expr.op))
        else:
            raise Exception('Unknown expression {}'.format(expr))

    def evaluateName (self, expr):
        return { expr.id: 1 }

    def evaluateInvert (self, expr):
        return self.evaluate(expr.operand)

    def evaluateBitOr (self, left, right):
        return self.join(self.evaluate(left), .5, self.evaluate(right), .5)

    def evaluateBitAnd (self, left, right):
        return self.join(self.evaluate(left), .5, self.evaluate(right), .5)

    def evaluateBitXor (self, left, right):
        return self.join(self.evaluate(left), .5, self.evaluate(right), .5)

    def join (self, a, ratioA, b, ratioB):
        d = { k: v * ratioA for k, v in a.items() }
        for k, v in b.items():
            if k in d:
                d[k] += v * ratioB
            else:
                d[k] = v * ratioB
        return d

expr = '((A&B)|(C&D)^~E)'
ee = ExpressionEvaluator(expr)
print(ee.run())
# > {'A': 0.25, 'C': 0.125, 'B': 0.25, 'E': 0.25, 'D': 0.125}

这个实现基本上会为给定的表达式生成一个普通的AST,并递归地遍历树并评估不同的运算符。大evaluate方法只是将工作委托给下面的类型特定方法;除了我们在这里返回每个节点的分析结果之外,它类似于ast.NodeVisitor所做的。可以增加节点而不是返回节点。

在这种情况下,评估仅基于表达式中的发生。我没有明确地检查语义效果。因此,对于表达式A | (A & B),我得到{'A': 0.75, 'B': 0.25},尽管有人可能认为语义B与结果完全没有关系(使其成为{'A': 1})。不过这是我要留给你的东西。截至目前,每个二进制操作的处理方式相同(每个操作数的相关性为50%),但当然可以调整以引入一些语义规则。

无论如何,没有必要实际测试变量赋值。

答案 1 :(得分:0)

不是重新发明轮子,而是像现有的性能和安全性一样陷入风险,最好是搜索业界已经很好接受的库。

sympy的{​​{3}} Logic Module会做出你想要实现的确切事情而不诉诸evil哦,我的意思是eval。更重要的是,因为布尔表达式是而不是字符串,所以你不必关心解析通常会成为瓶颈的表达式。

答案 2 :(得分:0)

您不必准备静态表来计算它。 Python是一种动态语言,因此它能够在运行时自行解释和运行代码。

在你的情况下,我会建议一个解决方案:

import random, re, time

#Step 1: Input your expression as a string
logic_exp = "A|B&(C|D)&E|(F|G|H&(I&J|K|(L&M|N&O|P|Q&R|S)&T)|U&V|W&X&Y)"

#Step 2: Retrieve all the variable names.
#        You can design a rule for naming, and use regex to retrieve them.
#        Here for example, I consider all the single-cap-lettler are variables.
name_regex = re.compile(r"[A-Z]")

#Step 3: Replace each variable with its value. 
#        You could get the value with reading files or keyboard input.
#        Here for example I just use random 0 or 1.
for name in name_regex.findall(logic_exp):
    logic_exp = logic_exp.replace(name, str(random.randrange(2)))

#Step 4: Replace the operators. Python use 'and', 'or' instead of '&', '|' 
logic_exp = logic_exp.replace("&", " and ")
logic_exp = logic_exp.replace("|", " or " )    


#Step 5: interpret the expression with eval(exp) and output its value.
print "exporession =", logic_exp  
print "expression output =",eval(logic_exp)

这将非常快,占用的内存非常少。对于测试,我使用25个输入变量运行上面的示例:

exporession = 1 or 1 and (1 or 1) and 0 or (0 or 0 or 1 and (1 and 0 or 0 or (0 and 0 or 0 and 0 or 1 or 0 and 0 or 0) and 1) or 0 and 1 or 0 and 1 and 0)
expression output= 1
computing time: 0.000158071517944 seconds     

根据您的评论,我发现您正在计算所有可能的组合而不是给定输入值的输出。如果是这样,它将成为典型的 NP-complete Boolean satisfiability problem。我认为没有任何算法可以通过低于O(2^N)的复杂度来实现。我建议您使用关键字fast algorithm to solve SAT problem进行搜索,您会发现很多有趣的事情。