Python: рекурсивно группировать операнды вместе со своими операторами в списке - PullRequest
0 голосов
/ 13 апреля 2020

У меня есть список, содержащий символы и операторы, например:

[['B31', '+', 'W311', '*', ['B21', '+', 'W211', '*', ['B11', '+', 'W111', '*', 'x'], '+', 'W221', '*',
                                       ['B12', '+', 'W121', '*', 'x']], '+', 'W312', '*',
             ['B22', '+', 'W212', '*', ['B11', '+', 'W111', '*', 'x'], '+', 'W222', '*',
              ['B12', '+', 'W121', '*', 'x']]]]

I wi sh для группировки операторов вместе с их операндами в списках из 3 элементов, здесь это будет

[['B31', '+',
          ['W311', '*',
           ['B21', '+',
            [['W211', '*', [['B11', '+', ['W111', '*', 'x']]],
             '+', ['W221', '*',
                   ['B12', '+', ['W121', '*', 'x']]]]]]]],
         '+', ['W312', '*',
               ['B22', '+',
                [['W212', '*', ['B11', '+', ['W111', '*', 'x']]],
                 '+', ['W222', '*',
                       ['B12', '+', ['W121', '*', 'x']]]]]]]]

Мой алгоритм выглядит следующим образом:

def group_by_symbol(formula: Union[List, str], symbol: str) -> List:
"""
Group multiplication in formula: a op b -> [a op b]
:param formula: contains operations not inside a list.
:return: operations enclosed in a list.
"""
modified_formula = formula

# loop backwards
for i in range(len(modified_formula) - 1, -1, -1):
    if i > len(modified_formula) - 1:
        continue

    if modified_formula[i] == symbol:
        # introduce parentheses around symbol
        group = [modified_formula[i - 1], modified_formula[i], modified_formula[i + 1]]
        del modified_formula[i:i + 2]
        modified_formula[i - 1] = group
    elif isinstance(modified_formula[i], List) \
            and len(modified_formula[i]) > 3:
        # recurse
        modified_formula[i] = group_by_symbol(modified_formula[i], symbol)

return modified_formula

Он называется следующим образом:

grouped = group_by_symbol(formula, '*')
grouped = group_by_symbol(grouped, '+')

Тем не менее, случай, когда в одном и том же добавлении более одного list не создает желаемых групп, и в результате я получаю следующий результат, где в списке содержится более одного символа +, а не все списки имеют размер 3:

[[['B31', '+', [['W311', '*', ['B21', '+', ['W211', '*', ['B11', '+', ['W111', '*', 'x']]], '+',
                                               ['W221', '*', ['B12', '+', ['W121', '*', 'x']]]]],
                                '+', ['W312', '*',
                                      ['B22', '+', ['W212', '*', ['B11', '+', ['W111', '*', 'x']]], '+',
                                       ['W222', '*', ['B12', '+', ['W121', '*', 'x']]]]]]]]]

Я подозреваю, что Ошибка имеет какое-то отношение к раннему выходу из рекурсии, однако проверка подсписка на наличие только строк в условии приводит к бесконечной рекурсии.

1 Ответ

1 голос
/ 13 апреля 2020

Мы можем значительно упростить программу, написав чистую функцию. Нумерованные комментарии здесь соответствуют номерам исходного кода в программе ниже.

  1. Если нет операции, op, мы достигли базового варианта. Если предоставленный аргумент arg является списком, преобразуйте его в выражение или просто верните arg.
  2. . По индукции является операцией, op. Если предоставленный arg является списком, мы также должны рекурсивно преобразовать его. Вернуть выражение из 3-х частей с expr(*arg), op и рекурсивным результатом expr(*more)
  3. По индукции есть операция, и поставляемое arg равно , а не список. Вернуть выражение из 3 частей с arg, op и рекурсивным результатом expr(*more)
tree = \
  [['B31','+','W311','*',['B21','+','W211','*',['B11','+','W111','*','x'],'+','W221','*',['B12','+','W121','*','x']],'+','W312','*',['B22','+','W212','*',['B11','+','W111','*','x'],'+','W222','*',['B12','+','W121','*','x']]]]

def expr(arg, op = None, *more):
  if not op:
    return expr(*arg) if isinstance(arg, list) else arg #1
  elif isinstance(arg, list):
    return [ expr(*arg), op, expr(*more) ]              #2
  else:
    return [ arg, op, expr(*more) ]                     #3


print(expr(tree))
# ['B31', '+', ['W311', '*', [['B21', '+', ['W211', '*', [['B11', '+', ['W111', '*', 'x']], '+', ['W221', '*', ['B12', '+', ['W121', '*', 'x']]]]]], '+', ['W312', '*', ['B22', '+', ['W212', '*', [['B11', '+', ['W111', '*', 'x']], '+', ['W222', '*', ['B12', '+', ['W121', '*', 'x']]]]]]]]]]

Может быть, мы сможем немного лучше проверить вывод, если преобразуем выражение в строку -

def expr_to_str(expr1, op, expr2):
  return \
  f"({expr_to_str(*expr1) if isinstance(expr1, list) else expr1} {op} {expr_to_str(*expr2) if isinstance(expr2, list) else expr2})"

print(expr_to_str(*expr(tree)))
# (B31 + (W311 * ((B21 + (W211 * ((B11 + (W111 * x)) + (W221 * (B12 + (W121 * x)))))) + (W312 * (B22 + (W212 * ((B11 + (W111 * x)) + (W222 * (B12 + (W121 * x))))))))))

Вот еще один способ использования class -

class expr:
  def __init__(self, x, op = None, *y):
    self.op = op
    self.x = expr(*x) if isinstance(x, list) else x
    self.y = expr(*y) if y else y

  def __str__(self):
    if not self.op:
      return f"{self.x}"
    else:
      return f"({self.x} {self.op} {self.y})"

print(expr(tree))
# (B31 + (W311 * ((B21 + (W211 * ((B11 + (W111 * x)) + (W221 * (B12 + (W121 * x)))))) + (W312 * (B22 + (W212 * ((B11 + (W111 * x)) + (W222 * (B12 + (W121 * x))))))))))

varaidi c поддержка

В комментарии вы спрашиваете, может ли expr поддерживать 3-элементные результаты и 2-элементные результаты. Вот одна из таких гибких реализаций -

В конструкторе __init__ мы делаем простой анализ случая -

  1. Если входные данные a - это список, а список меньше чем 4 элементов, нам не нужно ничего ломать. Просто отобразите expr на каждый элемент a.
  2. По индукции вход a представляет собой список не менее 4 элементов, поэтому нам нужно разбить его на более мелкие выражения , Создайте выражение первого элемента expr(a[0]), второго элемента expr(a[1]) и рекурсивный результат всех оставшихся элементов expr(a[2::])
  3. . По индукции входное значение a равно . не список, ie это один элемент. Установите для данных выражения одноэлементное значение [ a ]

В методе __str__ мы проводим аналогичный анализ для преобразования data нашего выражения в строку -

  1. Если self.data пусто, вернуть пустую строку, ""
  2. По индукции self.data является не пустым. Если оно меньше 2 элементов (синглтон), вернуть результат синглтона: f"{self.data[0]}"
  3. По индукции self.data равен как минимум 2 или более элементов. вернуть (...) -замкнутую строку, где каждая часть рекурсивно преобразуется в str и соединяется с пробелом, " "
class expr:
  def __init__(self, a):
    if isinstance(a, list):
      if len(a) < 4:
        self.data = [ expr(x) for x in a ]                   #1
      else:
        self.data = [ expr(a[0]), expr(a[1]), expr(a[2::]) ] #2
    else:
      self.data = [ a ]                                      #3

  def __str__(self):
    if not self.data:
      return ""                                              #1 empty
    elif len(self.data) < 2:
      return f"{self.data[0]}"                               #2 singleton
    else:
      return "(" + " ".join(str(x) for x in self.data) + ")" #3 variadic

print(expr(tree))
# (B31 + (W311 * ((B21 + (W211 * ((B11 + (W111 * x)) + (W221 * (B12 + (W121 * x)))))) + (W312 * (B22 + (W212 * ((B11 + (W111 * x)) + (W222 * (B12 + (W121 * x))))))))))

print(expr([[ "¬", ["a", "+", "b"]], "and", [["length", "x"], ">", 0]]))
# ((¬ (a + b)) and ((length x) > 0))

, разбивая ее

Разобрав сложную проблему на более мелкие части, легче решить подзадачи, что дает нам больше гибкости и контроля. Что бы это ни стоило, эта техника не использует механизмы Python, определенные c OOP. Это обычные, четко определенные, чистые функции -

def unit(): return ('unit',)
def nullary(op): return ('nullary', op)
def unary(op, a): return ('unary', op, a)
def binary(op, a, b): return ('binary', op, a, b)

Теперь, используя анализ плоского регистра, как мы делали ранее, мы реализуем наш конструктор рекурсивных выражений expr -

  1. , если вход a не является списком, это одно значение. построить nullary выражение с a
  2. По индукции вход представляет собой список. Если список пуст, создайте пустой результат, выражение unit.
  3. По индукции ввод не пустой список. Если он содержит ровно один элемент, создайте выражение nullary с единственным элементом, expr(a[0])
  4. По индукции вход содержит как минимум два элемента. Если на входе ровно два элемента, создайте выражение unary с expr(a[0]) и expr(a[1])
  5. . По индукции вход содержит как минимум три элемента. Если оператор ввода is_infix position, преобразовать в префикс позиции. Создайте выражение binary с expr(a[0]) и expr(a[1]) в поменяном положении, и рекурсивный результат expr(a[2::])
  6. По индукции вход содержит как минимум три элемента, как , а не в инфиксной позиции. Построить обычное (префиксная позиция) binary выражение expr(a[0]) и expr(a[1]) и рекурсивный результат expr(a[2::])
infix_ops = set([ '+', '-', '*', '/', '>', '<', 'and', 'or' ])

def is_infix (a):
  return a[1] in infix_ops

def expr(a):
  if not isinstance(a, list):
    return nullary(a)                                    #1
  elif len(a) == 0:
      return unit()                                      #2
  elif len(a) == 1:
    return nullary(expr(a[0]))                           #3
  elif len(a) == 2:
    return unary(expr(a[0]), expr(a[1]))                 #4
  elif is_infix(a):
    return binary(expr(a[1]), expr(a[0]), expr(a[2::]))  #5
  else:
    return binary(expr(a[0]), expr(a[1]), expr(a[2::]))  #6

Теперь, чтобы увидеть результат -

tree2 = \
  [[ "¬", ["a", "+", "b"]], "and", [["length", "x"], ">", 0]]

print(expr(tree2))
# ('binary', ('nullary', 'and'), ('unary', ('nullary', '¬'), ('binary', ('nullary', '+'), ('nullary', 'a'), ('nullary', ('nullary', 'b')))), ('nullary', ('binary', ('nullary', '>'), ('unary', ('nullary', 'length'), ('nullary', 'x')), ('nullary', ('nullary', 0)))))

Это просто одно из возможных представлений наших выражений. Поскольку мы реализовали наши выражения, используя tuple, Python может распечатать их, несмотря на то, что они многословны. В отличие от этого, вот как Python выбирает для представления объектов -

class foo: pass
f = foo()
print(f)
# <__main__.foo object at 0x7f2ba03bc8e0>

Здесь важно то, что наша структура данных выражений четко определена , и мы можем легко выполнить вычисления на ней или представить его другими способами -

def expr_to_str(m):
  if not isinstance(m, tuple):
    return str(m)
  elif m[0] == "unit":
    return ""
  elif m[0] == "nullary":
    return expr_to_str(m[1])
  elif m[0] == "unary":
    return f"({expr_to_str(m[1])} {expr_to_str(m[2])})"
  elif m[0] == "binary":
    return f"({expr_to_str(m[1])} {expr_to_str(m[2])} {expr_to_str(m[3])})"
  else:
    raise TypeError("invalid expression type", m[0])

print(expr_to_str(expr(tree2)))
# (and (¬ (+ a b)) (> (length x) 0))

вычисление выражения

Так что, если мы хотим оценить одно из наших выражений?

m = expr([3, "+", 2, "*", 5, "-", 1])

print(expr_to_str(m))
# (+ 3 (* 2 (- 5 1)))

print(eval_expr(m))
# 11

Вы в нескольких шагах от возможности писать eval_expr -

def eval_expr(m):
  if not isinstance(m, tuple):
      return m
  elif m[0] == "unit":
    return None
  elif m[0] == "nullary":
    return eval0(m[1])
  elif m[0] == "unary":
    return eval1(m[1], m[2])
  elif m[0] == "binary":
    return eval2(m[1], m[2], m[3])
  else:
    raise TypeError("invalid expression type", m[0])

Видите, сложные проблемы легче разбить на мелкие части. Теперь мы просто пишем eval0, eval1 и eval2 -

def eval0(op):
  return eval_expr(op)

def eval1(op, a):
  if op == expr("not"):      # or op == expr("¬") ...
    return not eval_expr(a)
  elif op == expr("neg"):    # or op == expr("~") ...
    return -eval_expr(a)
  # +, ++, --, etc...
  else:
    raise ValueError("invalid op", op)

def eval2(op, a, b):
  if op == expr("+"):
      return eval_expr(a) + eval_expr(b)
  elif op == expr("-"):
    return eval_expr(a) - eval_expr(b)
  elif op == expr("*"):
    return eval_expr(a) * eval_expr(b)
  elif op == expr("/"):
    return eval_expr(a) / eval_expr(b)
  elif op == expr("and"):
    return eval_expr(a) and eval_expr(b)
  # >, <, or, xor, etc...
  else:
    raise ValueError("invalid op", op)

Давайте теперь посмотрим на смесь выражений -

print(eval_expr(expr([True, 'and', ['not', False]])))
# True

print(eval_expr(expr(['neg', [9, '*', 11]])))
# -99

print(eval_expr(expr(['stay', '+', 'inside'])))
# 'stayinside'

Вы даже можете определить свои собственные функции -

def eval1(op, a):
  # ...
  elif op == expr('scream'):
    return eval_expr(a).upper() # make uppercase!
  else:
    raise ValueError("invalid op", op)

И использовать их в своих выражениях -

print(eval_expr(expr(["scream", ["stay", "+", "inside"]])))
# 'STAYINSIDE'
...