Не строгие аргументы по имени в Python?
Вопрос
Есть ли способ объявить аргументы функции не строгими (передаваемыми по имени)?
Если это невозможно напрямую: есть ли вспомогательные функции или декораторы, которые помогают мне достичь чего-то подобного?
Конкретный пример
Вот маленькая игрушка-пример для экспериментов.
Предположим, что я хочу создать крошечную библиотеку синтаксического анализатора, которая может справиться со следующей классической грамматикой для арифметических выражений с круглыми скобками (числа заменяются одним литеральным значением 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)
Это действительно позволяет мне записать грамматику так, чтобы это было очень близко к EBNF:
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
, но это не помогло создать такие объекты более кратким способом.
Итак, есть ли лучший способ передать аргументы по имени и избежать взрыва взаимно рекурсивно определенных значений?
2 ответа
Это хорошая идея, но это не то, что синтаксис Python позволяет: выражения Python всегда строго оцениваются (за исключением if
блоки и and
а также or
выражения короткого замыкания).
В частности, проблема в том, что в выражении типа:
num = p(lit("1"))
p
Аргумент функции всегда принимается с новым именем, привязанным к тому же объекту. Объект, полученный в результате оценки lit("1")
ничего не названо (пока имя не будет создано формальным параметром для p
), так что там нет имени для привязки. И наоборот, там должен быть какой-либо объект или p
не смог бы получить значение вообще.
То, что вы могли бы сделать, это добавить новый объект вместо лямбды, чтобы отложить оценку имени. Например, что-то вроде:
class DeferredNamespace(object):
def __init__(self, namespace):
self.__namespace = namespace
def __getattr__(self, name):
return DeferredLookup(self.__namespace, name)
class DeferredLookup(object):
def __init__(self, namespace, name):
self.__namespace = namespace
self.__name = name
def __getattr__(self, name):
return getattr(getattr(self.__namespace, self.__name), name)
d = DeferredNamespace(locals())
num = p(d.lit("1"))
В этом случае, d.lit
на самом деле не возвращается lit
, он возвращает DeferredLookup
объект, который будет использовать getattr(locals(), 'lit')
разрешить своих членов, когда они действительно используются. Обратите внимание, что это захватывает locals()
нетерпеливо, чего вы, возможно, не хотите; Вы можете адаптировать это для использования лямбды, или, что еще лучше, просто создать все свои сущности в каком-либо другом пространстве имен.
Вы все еще получаете бородавку d.
в синтаксисе, который может или не может быть решающим фактором, в зависимости от ваших целей с этим API.
Специальное решение для функций, которые должны принимать ровно один аргумент по имени
Если вы хотите определить функцию
f
который должен принимать один аргумент по имени, рассмотреть вопрос оf
в@decorator
, Вместо аргумента заваленыlambdas
затем декоратор может напрямую получить определение функции.
lambdas
в вопросе появляются потому, что нам нужен способ сделать выполнение правой стороны ленивым. Однако, если мы изменим определения нетерминальных символов на def
s, а не локальные переменные, RHS также не выполняется сразу. Тогда то, что мы должны сделать, это преобразовать эти def
с в ParserCombinator
как-то Для этого мы можем использовать декораторы.
Мы можем определить декоратор, который оборачивает функцию в LazyParserCombinator
следующее:
def rule(f):
return LazyParserCombinator(f)
и затем примените его к функциям, которые содержат определения каждого правила грамматики:
@rule
def num(): return lit("1")
@rule
def factor(): return num | (lit("(") + expr + lit(")"))
@rule
def term(): return factor + lit("*") + term | factor
@rule
def expr(): return (term + lit("+") + expr) | term
Синтаксические издержки в правой части правил минимальны (нет затрат на ссылки на другие правила, нет p(...)
-обертки или ruleName()
-безопасные скобки), и нет никакого интуитивно понятного шаблона с лямбдами.
Объяснение:
Учитывая функцию более высокого порядка h
мы можем использовать его для украшения другой функции f
следующее:
@h
def f():
<body>
Что это делает по сути:
def f():
<body>
f = h(f)
а также h
не ограничен возвратом функций, он также может возвращать другие объекты, такие как ParserCombinator
с выше.