Получение последовательностей открытия и закрытия верхнего уровня из строки Python

Проблема, которую я решаю, — это более сложный вариант объединения открывающих и закрывающих скобок.
Вместо сопоставления только по ([{}]), Мне дополнительно нужно сопоставить произвольные открывающие последовательности произвольной длины с закрывающими последовательностями произвольной длины, например '(' который отображается на ')'.

Мотивация:
Для синтаксического анализа грамматики в пользовательском синтаксическом анализаторе мне нужно различать скобочные литералы, которые окружены апострофами, и квадратные скобки на уровне грамматики, которые являются обычными скобками. Цель состоит в том, чтобы вернуть список всех найденных совпадений верхнего уровня. Совпадение представлено как набор из 4 элементов диапазона, охватываемого последовательностями открытия и закрытия.
В качестве мотивирующего примера оценим выражение id '(' [FArgs] ')' ['::' FunType] '{' VarDecl* Stmt+ '}' с отображением {'(': ')', '[': ']', '{': '}', "'('": "')'", "'['": "']'", "'{'": "'}'"} должен дать следующий список:

[(3, 6, 15, 18), (19, 20, 32, 33), (34, 37, 53, 56)]

Моей отправной точкой был обзор кода проверки сбалансированности скобок в Python, который я адаптировал в соответствии с дополнительными требованиями:

  • Соответствие произвольным последовательностям открытия и закрытия длиной> = 1
  • Соберите кортежи диапазонов, в которых находятся последовательности.

Вооруженный Python 3.9, это мой код:

def get_top_level_matching_pairs(expression: str, mapping: dict[str, str]) 
        -> list[tuple[int, int, int, int]]:
    """
    Returns all top-level matches of opening sequences to closing sequences.
    Each match is represented as a 4-tuple of the range that the opening and closing sequences span.

    >>>get_top_level_matching_pairs("(a) op (b) cl", {'(': ')', 'op': 'cl'})
    [(0, 1, 2, 3), (4, 6, 11, 13)]
    """
    def head_starts_with_one_from(match_targets: Union[KeysView[str], ValuesView[str]]) -> Optional[str]:
        # Check whether the expression, from index i, starts with one of the provided keys or values.
        # Return the first match found, none otherwise.
        return next(filter(lambda m: expression.startswith(m, i), match_targets), None)
    res = []
    queue = []  # functions as a stack to keep track of the opened pairs.
    start_index = None
    start_match = None
    i = 0
    while i < len(expression):
        if open := head_starts_with_one_from(mapping.keys()):
            if start_index is None:
                start_index = i
                start_match = open
            queue.append(mapping[open])  # Store the closing counterpart for easy comparisons
            i += len(open)
            continue
        if close := head_starts_with_one_from(mapping.values()):
            try:
                if (stack_head := queue.pop()) == close:
                    if not queue:  # This closing token closes a top-level opening sequence, so add the result
                        res.append((start_index, start_index + len(start_match), i, i + len(close)))
                        start_index = None
                        start_match = None
                    i += len(close)
                    continue
                # raise mismatched opening and closing characters.

            except IndexError:
                # raise closing sequence without an opening. (uses stack_head variable)
        i += 1
    return res

Мои вопросы:

  1. Должен ли я помещать предварительные условия в строку документации для функции или я должен проверить их в коде? Предварительные условия: строки в отображении не пусты, и ни одна строка отображения не может быть подмножеством другой.
  2. Это head_starts_with_one_from Вложенная функция оправдана как вложенная функция? Это позволяет аккуратно моржовые выражения на if open и if close линий.
  3. Есть ли лучший способ перебрать выражение, учитывая, что вам нужно одновременно сопоставить изменяющийся диапазон (здесь: 1 или 3 символа) выражения и иногда пропускать его часть?

Конечно, дополнительные комментарии приветствуются.

1 ответ
1

  • Я считаю head_starts_with_one_from было бы чище, используя выражение генератора:

    def head_starts_with_one_from(match_targets: Iterable[str]) -> Optional[str]:
        return next(
            (
                m
                for m in match_targets
                if expression.startswith(m, i)
            ),
            None
        )
    

    Или еще лучше, старый добрый цикл for.

    def head_starts_with_one_from(match_targets: Iterable[str]) -> Optional[str]:
        for m in match_targets:
            if expression.startswith(m, i):
                return m
        return None
    
  • Потому что вы не храните start_index в queue ваш код не может работать с вложенными скобками. Теперь мы избавились от необходимости start_index. Тебе действительно не нужно start_match см. следующий пункт.

    queue.append((i, mapping[open]))
    
  • Вы никогда не используете stack_head, поэтому вам не нужно использовать здесь оператор моржа.

  • Вы можете использовать elif и else избавиться от continueс.

  • Вы можете использовать операторы защиты, чтобы предотвратить анти-шаблон стрелки.

    if queue.pop() != close:
        # raise mismatched opening and closing characters.
    
def get_top_level_matching_pairs(
    expression: str,
    mapping: dict[str, str],
) -> list[tuple[int, int, int, int]]:
    def head_starts_with_one_from(match_targets: Iterable[str]) -> Optional[str]:
        for m in match_targets:
            if expression.startswith(m, i):
                return m
        return None
    res = []
    queue = []
    i = 0
    while i < len(expression):
        if open := head_starts_with_one_from(mapping.keys()):
            queue.append((i, mapping[open]))
            i += len(open)
        elif close := head_starts_with_one_from(mapping.values()):
            if not queue:
                # raise closing sequence without an opening.
            index, match = queue.pop()
            if match != close:
                # raise mismatched opening and closing characters.
            if not queue:  # This closing token closes a top-level opening sequence, so add the result
                res.append((index, index + len(match), i, i + len(close)))
            i += len(close)
        else:
            i += 1
    return res

  • Вы, кажется, подразумеваете, что нормальный цикл for предпочтительнее выражения генератора, которое само по себе предпочтительнее выражения фильтра. Почему этот заказ?

    — Итерниам

  • stack_head использовался в сообщении об ошибках, теперь я включил его в исходный код.

    — Итерниам

  • Код res.append((index, index + len(match), i, i + len(close))) подразумевает, что match отличается от close, чего никогда не бывает. Это приводит к сбою второго аргумента кортежа, если длина последовательности открытия не равна последовательности закрытия, поэтому start_match изначально был сохранен. Сломанный тестовый пример: expression='op a c', mapping={'op': 'c'}. Ожидается [(0, 2, 5, 6)], актуально [(0, 1, 5, 6)]

    — Итерниам


Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *