Pyparsing的setParseAction函数未获取任何参数。

16
我正在尝试解析一个简单的键值查询语言。实际上,我已经用一个巨大的怪物解析器完成了它,然后我通过第二次遍历对解析树进行了清理。我想做的是从底层开始进行干净的解析,其中包括使用集合来消除冗余的键值对等。虽然我之前已经让它工作了,但我感觉我没有完全理解pyparsing的行为方式,所以我做了很多变通方法等,有点逆着潮流而动。
目前,这是我“简化”的解析器的开头部分:
from pyparsing import *   

bool_act = lambda t: bool(t[0])
int_act  = lambda t: int(t[0])

def keyval_act(instring, loc, tokens):
    return set([(tokens.k, tokens.v)])

def keyin_act(instring, loc, tokens):
    return set([(tokens.k, set(tokens.vs))])

string = (
      Word(alphas + '_', alphanums + '_')
    | quotedString.setParseAction( removeQuotes )
    )
boolean = (
      CaselessLiteral('true')
    | CaselessLiteral('false')
    )
integer = Word(nums).setParseAction( int_act )
value = (
      boolean.setParseAction(bool_act)
    | integer
    | string
    )
keyval = (string('k') + Suppress('=') + value('v')
          ).setParseAction(keyval_act)
keyin = (
    string('k') + Suppress(CaselessLiteral('in')) +
    nestedExpr('{','}', content = delimitedList(value)('vs'))
    ).setParseAction(keyin_act)

grammar = keyin + stringEnd | keyval + stringEnd

"语法"非终结符目前仅是一个存根,我将最终向键中添加可嵌套的联结和析取,以便可以解析此类搜索:

a = 1, b = 2 , c in {1,2,3} | d = 4, ( e = 5 | e = 2, (f = 3, f = 4))

目前,我还不太明白pyparsing如何调用我的setParseAction函数。我知道在传递参数方面有一些魔法,但是我遇到了一个错误,即根本没有将任何参数传递给该函数。因此,如果我执行以下操作:

grammar.parseString('hi in {1,2,3}')

我遇到了这个错误:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.6/site-packages/pyparsing.py", line 1021, in parseString
    loc, tokens = self._parse( instring, 0 )
  File "/usr/lib/python2.6/site-packages/pyparsing.py", line 894, in _parseNoCache
    loc,tokens = self.parseImpl( instring, preloc, doActions )
  File "/usr/lib/python2.6/site-packages/pyparsing.py", line 2478, in parseImpl
    ret = e._parse( instring, loc, doActions )
  File "/usr/lib/python2.6/site-packages/pyparsing.py", line 894, in _parseNoCache
    loc,tokens = self.parseImpl( instring, preloc, doActions )
  File "/usr/lib/python2.6/site-packages/pyparsing.py", line 2351, in parseImpl
    loc, resultlist = self.exprs[0]._parse( instring, loc, doActions, callPreParse=False )
  File "/usr/lib/python2.6/site-packages/pyparsing.py", line 921, in _parseNoCache
    tokens = fn( instring, tokensStart, retTokens )
  File "/usr/lib/python2.6/site-packages/pyparsing.py", line 675, in wrapper
    return func(*args[limit[0]:])
TypeError: keyin_act() takes exactly 3 arguments (0 given)

从回溯信息可以看出,我正在使用Python2.6和pyparsing 1.5.6。

有人能否给我一些见解,为什么该函数没有获得正确数量的参数?


这段代码真的需要注释来帮助读者。 - Paragon
大部分都是标准的pyparsing代码,您需要澄清哪一部分? - deontologician
它看起来很不易读。我开始查看代码,但是停下来了,因为花太长时间去弄清楚那些没有描述性名称的函数的目的。 - Paragon
2个回答

21

最新版本的setParseAction确实做了一些额外的魔法,但不幸的是这牺牲了一些开发简易性。现在,setParseAction中的参数检测逻辑依赖于解析操作引发异常直到它被正确地调用,并从3开始逐渐减少至0,此后就放弃并引发您看到的异常。

除非在这种情况下,来自解析操作的异常不是由于参数列表不匹配,而是由于代码中的真正错误。为了更好地查看此内容,请将通用的try-except插入到解析操作中:

def keyin_act(instring, loc, tokens): 
    try:
        return set([(tokens.k, set(tokens.vs[0]))]) 
    except Exception as e:
        print e

然后你会得到:

unhashable type: 'set'

事实上,您从中创建返回集的列表的第二个元素本身是一个可变容器即集合,因此不可哈希以包含在集合中。如果您将其更改为使用frozenset,则会得到:

[set([('hi', frozenset([]))])]
为什么frozenset为空?我建议你将结果名称“vs”的位置更改为:
nestedExpr('{','}', content = delimitedList(value))('vs') 

现在解析 'hi in {1,2,3}' 返回的结果为:

[set([('hi', frozenset([([1, 2, 3], {})]))])]

这有点混乱,如果我们在您的解析操作顶部删除此行,您将看到不同命名结果实际包含的内容:

print tokens.dump()

我们得到:

['hi', [1, 2, 3]]
- k: hi
- vs: [[1, 2, 3]]

'vs'实际上指向一个包含列表的列表。因此,我们可能希望从tokens.vs[0]构建我们的集合,而不是tokens.vs。现在我们解析的结果如下:

[set([('hi', frozenset([1, 2, 3]))])]

以下是有关语法的其他一些提示:

  • 尝试使用CaselessKeyword而不是CaselessLiteral。对于语法关键字,关键字是更好的选择,因为它们本质上避免了将“inside”的前导“in”误认为是语法中的关键字“in”。

  • 不确定您从解析操作中返回集合的目的 - 对于键值对,元组可能更好,因为它会保留标记的顺序。在程序的解析后阶段建立键和值的集合。

  • 对于其他语法调试工具,请查看setDebug和traceParseAction装饰器。


2
非常感谢!一般来说,为了避免pyparsing隐藏异常抛出的原因,您需要在parseAction函数中捕获任何异常。这样说准确吗? - deontologician
1
鉴于解析操作规范的新实现,通常情况下遵循这个指南是一个不错的选择。然而,有时候你需要让解析操作引发异常,比如在进行语义验证时,这种情况下你的代码应该引发一个 pyparsing 的 ParseException 异常。 - PaulMcG
2
这个有用的答案应该被包含在 setParseAction() 方法的文档中。 - cfi
2
为什么他们不使用内省来查找解析操作使用的参数数量?或者至少捕获TypeError,这样至少他们不会错误标记每个异常? - Ant6n
那个检测参数的装饰器真是太恶心了。非常感谢。 - Lynn
检测参数的代码已被重写,不再遮盖解析操作代码中的异常。 - PaulMcG

5

Paul已经解释了根本问题:由您的解析操作引发的TypeError会混淆pyparsing自动确定解析操作所需参数数量的方式。

以下是我用来避免这种混淆的方法:使用装饰器,如果再次调用带有较少参数的函数,则重新引发任何抛出的TypeError

import functools
import inspect
import sys

def parse_action(f):
    """
    Decorator for pyparsing parse actions to ease debugging.

    pyparsing uses trial & error to deduce the number of arguments a parse
    action accepts. Unfortunately any ``TypeError`` raised by a parse action
    confuses that mechanism.

    This decorator replaces the trial & error mechanism with one based on
    reflection. If the decorated function itself raises a ``TypeError`` then
    that exception is re-raised if the wrapper is called with less arguments
    than required. This makes sure that the actual ``TypeError`` bubbles up
    from the call to the parse action (instead of the one caused by pyparsing's
    trial & error).
    """
    num_args = len(inspect.getargspec(f).args)
    if num_args > 3:
        raise ValueError('Input function must take at most 3 parameters.')

    @functools.wraps(f)
    def action(*args):
        if len(args) < num_args:
            if action.exc_info:
                raise action.exc_info[0], action.exc_info[1], action.exc_info[2]
        action.exc_info = None
        try:
            return f(*args[:-(num_args + 1):-1])
        except TypeError as e:
            action.exc_info = sys.exc_info()
            raise

    action.exc_info = None
    return action

以下是如何使用它的步骤:
from pyparsing import Literal

@parse_action
def my_parse_action(tokens):
    raise TypeError('Ooops')

x = Literal('x').setParseAction(my_parse_action)
x.parseString('x')

这将为您提供:
Traceback (most recent call last):
  File "test.py", line 49, in <module>
    x.parseString('x')
  File "/usr/local/lib/python2.7/dist-packages/pyparsing-2.0.2-py2.7.egg/pyparsing.py", line 1101, in parseString
    loc, tokens = self._parse( instring, 0 )
  File "/usr/local/lib/python2.7/dist-packages/pyparsing-2.0.2-py2.7.egg/pyparsing.py", line 1001, in _parseNoCache
    tokens = fn( instring, tokensStart, retTokens )
  File "/usr/local/lib/python2.7/dist-packages/pyparsing-2.0.2-py2.7.egg/pyparsing.py", line 765, in wrapper
    ret = func(*args[limit[0]:])
  File "test.py", line 33, in action
    return f(*args[:num_args])
  File "test.py", line 46, in my_parse_action
    raise TypeError('Ooops')
TypeError: Ooops

与没有使用@parse_action装饰器时得到的回溯信息相比较:
Traceback (most recent call last):
  File "test.py", line 49, in <module>
    x.parseString('x')
  File "/usr/local/lib/python2.7/dist-packages/pyparsing-2.0.2-py2.7.egg/pyparsing.py", line 1101, in parseString
    loc, tokens = self._parse( instring, 0 )
  File "/usr/local/lib/python2.7/dist-packages/pyparsing-2.0.2-py2.7.egg/pyparsing.py", line 1001, in _parseNoCache
    tokens = fn( instring, tokensStart, retTokens )
  File "/usr/local/lib/python2.7/dist-packages/pyparsing-2.0.2-py2.7.egg/pyparsing.py", line 765, in wrapper
    ret = func(*args[limit[0]:])
TypeError: my_parse_action() takes exactly 1 argument (0 given)

我已经添加了一些代码到 setParseAction 中,试图使其不那么容易受到 TypeError 混淆的影响 - 这个解决方案在最新版本的 pyparsing 中仍然适用吗? - PaulMcG
@PaulMcGuire:那是哪个版本?PyPI上的最新版本主页上的版本是2.0.3,但在其变更日志中没有关于setParseAction的任何信息。 - Florian Brucker
2
@FlorianBrucker 我宣布这个问题“已解决”,基本修复在2016年2月发布的2.1.0版本中完成,此后还进行了一些增量跟进修复以解决Python 3.x库中的变化。截至最新版本2.1.10,于2016年10月发布,现在应该基本恢复正常了。感谢您发布这个解决方法来度过混乱时期 - 我希望您现在能够直接使用pyparsing的解析操作。 - PaulMcG
@PaulMcGuire:太好了,感谢您的修复和更新! - Florian Brucker
1
@JohnGreene:根据上面PaulMCG的评论,由于pyparsing中的基础问题已经得到解决,因此装饰器现在应该不再必要。 - Florian Brucker
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接