Вопрос
Есть ли способ объявить аргументы функции нестрогими (передаются по имени) ?
Если это невозможно напрямую: есть ли какие-либо вспомогательные функции или декораторы, которые помогут мне добиться чего-то подобного?
Конкретный пример
Вот небольшая игрушка-пример для экспериментов.
Предположим, что я хочу создать крошечную библиотеку синтаксического анализатора-комбинатора, которая может справиться со следующей классической грамматикой для арифметических выражений со скобками (числа заменены одним литеральным значением 1
для простоты):
num = "1"
factor = num
| "(" + expr + ")"
term = factor + "*" + term
| factor
expr = term + "+" + expr
| term
Предположим, что я определяю комбинатор синтаксического анализатора как объект, у которого есть метод parse
, который может принимать список токенов, текущую позицию и либо выдавать ошибку синтаксического анализа, либо возвращать результат и новую позицию. Я могу красиво определить базовый класс ParserCombinator
, который обеспечивает +
(конкатенацию) и |
(альтернативу). Затем я могу определить комбинаторы парсеров, которые принимают константные строки, и реализовать +
и |
:
# Two kinds of errors that can be thrown by a parser combinator
class UnexpectedEndOfInput(Exception): pass
class ParseError(Exception): pass
# Base class that provides methods for `+` and `|` syntax
class ParserCombinator:
def __add__(self, next):
return AddCombinator(self, next)
def __or__(self, other):
return OrCombinator(self, other)
# Literally taken string constants
class Lit(ParserCombinator):
def __init__(self, string):
self.string = string
def parse(self, tokens, pos):
if pos < len(tokens):
t = tokens[pos]
if t == self.string:
return t, (pos + 1)
else:
raise ParseError
else:
raise UnexpectedEndOfInput
def lit(str):
return Lit(str)
# Concatenation
class AddCombinator(ParserCombinator):
def __init__(self, first, second):
self.first = first
self.second = second
def parse(self, tokens, pos):
x, p1 = self.first.parse(tokens, pos)
y, p2 = self.second.parse(tokens, p1)
return (x, y), p2
# Alternative
class OrCombinator(ParserCombinator):
def __init__(self, first, second):
self.first = first
self.second = second
def parse(self, tokens, pos):
try:
return self.first.parse(tokens, pos)
except:
return self.second.parse(tokens, pos)
Пока все в порядке. Однако, поскольку нетерминальные символы грамматики определяются взаимно рекурсивным образом, и я не могу быстро развернуть дерево всех возможных комбинаций синтаксических анализаторов, мне приходится работать с фабриками комбинаторов синтаксических анализаторов и оберните их примерно так:
# Wrapper that prevents immediate stack overflow
class LazyParserCombinator(ParserCombinator):
def __init__(self, parserFactory):
self.parserFactory = parserFactory
def parse(self, tokens, pos):
return self.parserFactory().parse(tokens, pos)
def p(parserFactory):
return LazyParserCombinator(parserFactory)
Это действительно позволяет мне записать грамматику очень близко к РБНФ:
num = p(lambda: lit("1"))
factor = p(lambda: num | (lit("(") + expr + lit(")")))
term = p(lambda: (factor + lit("*") + term) | factor)
expr = p(lambda: (term + lit("+") + expr) | term)
И это действительно работает:
tokens = [str(x) for x in "1+(1+1)*(1+1+1)+1*(1+1)"]
print(expr.parse(tokens, 0))
Однако p(lambda: ...)
в каждой строке немного раздражает. Есть ли какой-то идиоматический способ избавиться от него? Было бы неплохо, если бы можно было каким-то образом передать всю RHS правила "по имени", не вызывая нетерпеливого вычисления бесконечной взаимной рекурсии.
Что я пробовал
Я проверил, что доступно в основном языке: кажется, что только if
, and
и or
могут "замыкаться", пожалуйста, поправьте меня, если я ошибаюсь.
Я пытался посмотреть, как это делают другие библиотеки, не относящиеся к игрушкам.
Например, funcparserlib использует явные предварительные объявления, чтобы избежать взаимной рекурсии (посмотрите на часть
forward_decl
иvalue.define
в github). пример кода README.md).parsec.py
использует некоторые специальные декораторы@generate
и, кажется, делает что-то вроде монадического синтаксического анализа с использованием сопрограмм. Все это очень хорошо, но моя цель — понять, какие варианты у меня есть в отношении основных стратегий оценки, доступных в Python.
Я также нашел что-то вроде lazy_object_proxy.Proxy
, но это не похоже чтобы помочь создавать экземпляры таких объектов более кратким способом.
Итак, есть ли более удобный способ передавать аргументы по имени и избегать искажения взаимно рекурсивно определенных значений?