используйте собственное логическое выражение в операторе if

#python #python-3.x #logical-operators #boolean-logic

Вопрос:

Я получил выражение, проанализированное с помощью pyparsing, чтобы восстановить его как своего рода логическое дерево:

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

A, B и C-это ключи в дикте со строковыми значениями (без логических значений!)

OR_ (объединение) и AND_ (пересечение) являются только именами классов и ничего не делают rn, я подумываю о том, чтобы поместить оценщика в эти классы.

теперь мой вопрос в том, как мне превратить это проанализированное выражение в то, которое может оценить 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 именно это меня беспокоит, потому что я не могу придумать другого способа написать это.

Комментарии:

1. Я думаю, что вы должны сначала сделать значения dict фактическими наборами, например: 'A': {'Hi', 'No', 'Yes'} . Затем напишите код, чтобы взять проанализированное выражение OR_:[AND_:['A', 'B'] , 'C'] и оценить его, используя стек и традиционные операции набора Python, где, например, AND_:['A', 'B'] вычисляется до dict['A'].intersection(dict['B']) или просто dict['A'] amp; dict['B'] . Аналогично для союза.

2. Это что-то особенное для использования pyparsing ? Я не совсем уверен, чего именно пытается достичь ваш код.

3. @jarmond ага, я вижу, это действительно идет в том направлении, о котором я думаю. Я просто немного запутался в том, как повторить это несколько раз в сочетании с объединением и исключением …

4. @KyleParsons э-э-э, я бы не сказал, что это специфично для pyparsing, но pyparsing помогает в анализе выражений, я думаю. Это не обязательно должен быть синтаксический анализ, но это единственный синтаксический анализатор, который я мог придумать. моя цель состоит в том, чтобы ввести любое выражение с и/или/без, и на основе этого оно должно выводить все значения, допустимые для выражения

Ответ №1:

Мы можем проанализировать и перевести наше выражение с помощью ast модуля. Сначала мы анализируем наш оператор , а затем определяем преобразователь узлов , который обменивается and amp; 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 должна возникнуть ошибка, если пользователь предоставит что-то более сложное, чем просто and s и or s, но я бы очень настороженно относился к этому коду в производственной ситуации.

Во-вторых, при переводе and на amp; or на ) возникает небольшая сложность | из-за того, как Python представляет цепочку and s по сравнению с цепочкой amp; s. Цепочка and s становится одним BoolOp узлом с несколькими значениями, в то время как цепочка amp; BinOp s становится вложенной, каждая из которых имеет левое и правое значение. Сравнить

 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 amp; B amp; 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. Добавьте дополнительный аргумент для определения универсума дискурса. Добавьте a visit_UnaryOp not 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)
 

Вы заставляете своих пользователей предварительно анализировать выражение, которое они вам дают, но вы избавляете себя от множества беспокойств и проблем.

Комментарии:

1. Спасибо за ваше решение! Я все еще пытаюсь понять код, но он действительно выполняет то, что я хочу, с помощью И и Или. Поскольку он вообще не использует pyparsing, я должен посмотреть, смогу ли я просто избавиться от этой идеи, но все равно я очень благодарен 🙂

Ответ №2:

Вы можете использовать двоичные операторы, если преобразуете их в наборы:

 output = list(set(a) amp; set(b) | set(c))
 

Комментарии:

1. спасибо за ответ! но я ищу более гибкое выражение, так как выражение зависит от пользовательского ввода, поэтому выражение может быть любым сочетанием с и/или/нет

2. Я думаю, что семантика not неопределенна, если мы не находимся в какой-то вселенной возможностей, которые мы где-то определяем. На что следует 'not A' оценивать?

3. @KyleParsons not A должен приводить в основном ко всем элементам, отличным от элементов в

4. Конечно, но в каком контексте? Если A = ['Ok', 'No'] тогда существует not A == ['', 'a', 'b', ..., 'aa', 'ab', ...] # every string but 'Ok' and 'No' или неявно предполагается, что вселенная возможностей является объединением всех значений в вашем словаре?

5. @KyleParsons О, извините, я только что заметил, что забыл упомянуть sth, есть еще один список, содержащий все элементы из других списков. В основном not A в этом случае будет сравнение ВСЕХ с A и результатом ['Why', 'Okay'] (не включая элементы из A, но все остальные). Надеюсь, в этом больше смысла 🙂