Проблема, которую я решаю, — это более сложный вариант объединения открывающих и закрывающих скобок.
Вместо сопоставления только по ([{}]), Мне дополнительно нужно сопоставить произвольные открывающие последовательности произвольной длины с закрывающими последовательностями произвольной длины, например '(' который отображается на ')'.
Мотивация:
Для синтаксического анализа грамматики в пользовательском синтаксическом анализаторе мне нужно различать скобочные литералы, которые окружены апострофами, и квадратные скобки на уровне грамматики, которые являются обычными скобками. Цель состоит в том, чтобы вернуть список всех найденных совпадений верхнего уровня. Совпадение представлено как набор из 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
Мои вопросы:
- Должен ли я помещать предварительные условия в строку документации для функции или я должен проверить их в коде? Предварительные условия: строки в отображении не пусты, и ни одна строка отображения не может быть подмножеством другой.
- Это
head_starts_with_one_fromВложенная функция оправдана как вложенная функция? Это позволяет аккуратно моржовые выражения наif openиif closeлиний. - Есть ли лучший способ перебрать выражение, учитывая, что вам нужно одновременно сопоставить изменяющийся диапазон (здесь: 1 или 3 символа) выражения и иногда пропускать его часть?
Конечно, дополнительные комментарии приветствуются.
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)]— Итерниам