3

问题

有没有办法将函数参数声明为非严格的(按名称传递)?

如果这不能直接实现:是否有任何辅助函数或装饰器可以帮助我实现类似的目标?


具体例子

这是一个可以试验的小玩具示例。

假设我想构建一个小型解析器组合器库,它可以处理以下带括号的算术表达式的经典语法(1为简单起见,将数字替换为单个文字值):

num    = "1"

factor = num 
       | "(" + expr + ")"

term   = factor + "*" + term 
       | factor

expr   = term + "+" + expr
       | term

假设我将解析器组合器定义为一个对象,该对象具有一个方法,该方法parse可以获取令牌列表、当前位置,并且要么引发解析错误,要么返回结果和新位置。我可以很好地定义一个ParserCombinator提供+(连接)和|(替代)的基类。然后我可以定义接受常量字符串的解析器组合器,并实现+and |

# 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 使用显式前向声明来避免相互递归(查看 github README.md 示例代码中的forward_declandvalue.define 部分)。

  • 使用parsec.py一些特殊的@generate的装饰器,并且似乎使用协程进行单子解析。这一切都很好,但我的目标是了解我对 Python 中可用的基本评估策略有哪些选择。

我还发现了类似的东西lazy_object_proxy.Proxy,但它似乎并没有帮助以更简洁的方式实例化这些对象。

那么,有没有更好的方法来按名称传递参数并避免相互递归定义的值的爆炸?

4

2 回答 2

2

这是一个好主意,但 Python 的语法不允许这样做:Python 表达式总是被严格评估(if块和and短路or表达式除外)。

特别是,问题在于在如下表达式中:

num = p(lit("1"))

函数参数总是通过p绑定到同一个对象的新名称来接收。评估产生的对象lit("1")没有任何名称(直到通过形式参数 to 创建名称p),因此没有名称可以绑定。相反,那里必须有一个对象,否则p根本无法接收到值。

您可以做的是添加一个要使用的新对象而不是 lambda 来推迟对名称的评估。例如,类似:

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()会急切地捕获您可能不想要的;您可以对其进行调整以使用 lambda,或者更好的是,无论如何只需在其他命名空间中创建所有实体。

您仍然会d.在语法中遇到问题,这可能会或可能不会破坏交易,具体取决于您使用此 API 的目标。

于 2018-04-05T14:59:28.273 回答
1

必须只接受一个按名参数的函数的特殊解决方案

如果要定义一个f必须按名称接受一个参数的函数,请考虑将f其制成@decorator. lambdas装饰器可以直接接收函数定义,而不是散布的参数。


出现lambdas问题是因为我们需要一种方法来使右侧的执行变得懒惰。但是,如果我们将非终结符号的定义更改为defs 而不是局部变量,则 RHS 也不会立即执行。那么我们要做的就是以某种方式将这些defs转换成s。ParserCombinator为此,我们可以使用装饰器。


我们可以定义一个将函数包装成 a 的装饰器,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()- 括号),并且没有带有 lambda 的反直觉样板。


解释:

给定一个高阶函数h,我们可以用它来装饰其他函数f,如下所示:

@h
def f():
  <body>

这基本上是:

def f():
  <body>

f = h(f)

并且h不限于返回函数,它还可以返回其他对象,如ParserCombinator上面的 s。

于 2018-04-05T20:50:40.067 回答