Не строгие аргументы по имени в 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 в вопросе появляются потому, что нам нужен способ сделать выполнение правой стороны ленивым. Однако, если мы изменим определения нетерминальных символов на defs, а не локальные переменные, 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с выше.

Другие вопросы по тегам