#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
оценить? Я вижу два возможных решения:
- Добавьте дополнительный аргумент для определения универсума дискурса. Добавьте 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)
Вы заставляете своих пользователей предварительно анализировать выражение, которое они вам дают, но вы избавляете себя от множества беспокойств и проблем.
Комментарии:
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, но все остальные). Надеюсь, в этом больше смысла 🙂