代码之家  ›  专栏  ›  技术社区  ›  Andrey Tyukin

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

  •  3
  • Andrey Tyukin  · 技术社区  · 8 年前

    问题

    有没有办法将函数参数声明为非严格(已传递 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,而不触发对无限相互递归的热切求值,那就太好了。


    我试过的

    我已经检查了核心语言中的可用内容:似乎只有 if , and or 可以“短路”,如果我错了,请纠正我。

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

    • 例如 funcparserlib 使用显式前向声明避免相互递归 (请看 forward_decl value.define 参与github自述。md示例代码)。

    • 这个 parsec.py 使用一些特殊的 @generate 装饰师 并且似乎使用协同程序进行类似于一元语法分析的操作。 这很好,但我的目标是了解哪些选项 关于可用的基本评估策略 在Python中。

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

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

    2 回复  |  直到 8 年前
        1
  •  2
  •   Daniel Pryden    8 年前

    这是一个好主意,但Python的语法并不允许这样做:Python表达式总是经过严格计算(除了 if 块和 and or 短路表达式)。

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

    num = p(lit("1"))
    

    这个 p 函数参数总是与绑定到同一对象的新名称一起接收。评估产生的对象 lit("1") 不是 已命名 任何内容(直到通过形式参数创建名称 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的目标。

        2
  •  1
  •   Andrey Tyukin    8 年前

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

    如果要定义函数 f 必须按名称接受一个参数,考虑 f 变成一个 @decorator . 而不是争吵 lambdas ,则装饰器可以直接接收函数定义。


    这个 兰巴斯 这个问题之所以出现,是因为我们需要一种让右手边的执行变得懒惰的方法。但是,如果我们将非终端符号的定义更改为 def 而不是局部变量,RHS也不会立即执行。那么我们要做的就是转换这些 def公司 s进入 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 s以上。

    推荐文章