【问题标题】:use own logic expression in an if statement在 if 语句中使用自己的逻辑表达式
【发布时间】:2021-10-16 02:27:15
【问题描述】:

我得到了一个用 pyparsing 解析的表达式,以将其重构为一种布尔树:

expr = '(A and B) or C'
parsed -> OR_:[AND_:['A', 'B'] , 'C']

A、B 和 C 是带有字符串值的字典中的键(没有布尔值!)

OR_(联合)和 AND_(交集)只是类名,不做任何事情。我正在考虑在这些类中放置一个评估器。

现在我的问题是,如何将这个解析后的表达式转换为 Python 可以计算的表达式? 我正在尝试做的是获取一些字符串值并查看它是否满足整个表达式的条件,或者让它遍历每个子表达式并将其附加到结果列表中。

例子:

dict: {'A': ['Hi', 'No', 'Yes'], 'B': ['Why', 'No', 'Okay'], 'C': ['Okay']}
expression = '(A and B) or C'
if value in expression:
 output.append(value)
output -> ['No', 'Okay']  #intersection of A and B, union of that with C

差不多是这样,但是if value in expression这部分让我很困扰,因为我想不出任何其他的写法。

【问题讨论】:

  • 我认为您应该首先将 dict 值设为实际集,例如:'A': {'Hi', 'No', 'Yes'}。然后编写代码来获取解析后的表达式 OR_:[AND_:['A', 'B'] , 'C'] 并使用堆栈和传统的 Python 集合操作计算它,例如,AND_:['A', 'B'] 计算为 dict['A'].intersection(dict['B']) 或简单地 dict['A'] & dict['B']。联合类似。
  • 这是特定于使用 pyparsing 的东西吗?我不确定您的代码到底想达到什么目的。
  • @jarmond ahaa 我明白了,这确实符合我的想法。我只是有点困惑如何结合联合和排除多次迭代......
  • @KyleParsons 呃我不会说它特定于 pyparsing,但 pyparsing 有助于解析我猜想的表达式。它不一定是 pyparsing,但它是我能想到的唯一解析器。我的目标是输入任何带有 and/or/not 的表达式,并基于它应该输出对表达式有效的所有值

标签: python python-3.x logical-operators boolean-logic


【解决方案1】:

我们可以使用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 转换为&amp;(以及将or 转换为|)会有些复杂,因为Python 如何表示ands 链与&amp;s 链。 ands 链成为具有多个值的单个 BoolOp 节点,而 &amp; 链成为嵌套 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 应该评估什么?我看到了两种可能的解决方案:

  1. 添加一个额外的参数来指定论域。添加visit_UnaryOpnot A 翻译成set(U) - set(A) 之类的东西,其中U 是话语的宇宙。
  2. 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)

你强迫你的用户预先解析他们给你的表达式,但你可以省去很多担心和麻烦。

【讨论】:

  • 感谢您的解决方案!我仍在尝试理解代码,但它确实用 And 和 Or 完成了我想要的。由于它根本不使用 pyparsing,我得看看我是否可以处理这个最初的想法,但是,我仍然非常感激 :)
【解决方案2】:

如果将其转换为集合,则可以使用二元运算符:

output = list(set(a) & set(b) | set(c))

【讨论】:

  • 感谢您的回答!但我正在寻找更灵活的东西,因为表达式取决于用户输入,因此表达式可以是和/或/不的任意组合
  • 我认为not 的语义是未定义的,除非我们处于我们在某处定义的可能性的宇宙中。 'not A' 应该评估什么?
  • @KyleParsons not A 应该导致除了 A 中的元素之外的所有元素
  • 当然,但在什么情况下?如果A = ['Ok', 'No'] 则为not A == ['', 'a', 'b', ..., 'aa', 'ab', ...] # every string but 'Ok' and 'No' 或者是否隐含地假定所有可能性为字典中所有值的并集?
  • @KyleParsons 哦,抱歉,我刚刚注意到我错过了提及某事,还有另一个列表包含其他列表中的所有元素。基本上not A 在这种情况下会将 ALL 与 A 进行比较并导致 ['Why', 'Okay'] (不包括来自 A 的元素,但每个其他元素)。希望这更有意义:)
猜你喜欢
  • 1970-01-01
  • 2014-06-21
  • 1970-01-01
  • 1970-01-01
  • 2014-06-26
  • 2018-07-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多