Python中的非严格按名称参数?

时间:2018-04-05 14:27:32

标签: python parser-combinators callbyname evaluation-strategy

问题

有没有办法将函数参数声明为非严格(传递by-name)?

如果直接无法做到这一点:是否有帮助我实现类似功能的辅助函数或装饰器?

具体示例

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

假设我想构建一个小的解析器 - 组合器库,它可以处理带括号的算术表达式的以下经典语法(为简单起见,数字由单个字面值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&#34; by-name&#34;,而不会触发对无限相互递归的热切评估,那将是很好的。

我尝试了什么

我已经检查了核心语言中可用的内容:似乎只有ifandor可以&#34;短路&# 34;如果我错了,请纠正我。

我已经尝试过了解其他非玩具示例库是如何做到这一点的。

  • 例如, funcparserlib 使用显式前向声明来避免相互递归 (查看forward_declvalue.define  部分在github README.md示例代码中。

  • parsec.py使用了一些特殊的@generate装饰器 并且似乎使用协同程序执行类似monadic解析的操作。 这一切都非常好,但我的目标是了解哪些选项 我对可用的基本评估策略有所了解 在Python中。

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

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

2 个答案:

答案 0 :(得分:2)

这是一个好主意,但它并不是Python的语法所允许的:Python表达式总是被严格评估(除了if块和{{1 }和and短路表达式。)

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

or

始终使用绑定到同一对象的新名称接收num = p(lit("1")) 函数参数。评估p得到的对象不是命名任何东西(直到名称由形式参数创建到lit("1")),因此没有要绑定的名称。相反,那里必须有一个对象,否则p将无法接收到任何值。

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

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'),这可能是您不想要的;你可以调整它来使用lambda,或者更好的是,无论如何只需在其他命名空间中创建所有实体。

根据您使用此API的目标,您仍会在语法中得到locals()的疣,这可能会也可能不会成为交易破坏者。

答案 1 :(得分:1)

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

  

如果要定义必须按名称单个参数的函数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() - 需要括号),并且没有反直觉带有lambdas的样板。

说明:

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

@h
def f():
  <body>

这实际上是做什么的:

def f():
  <body>

f = h(f)

h不限于返回函数,它也可以返回其他对象,例如上面的ParserCombinator