2012-04-16 6 views
15

Ich versuche, eine einfache key = Wert Abfragesprache zu analysieren. Ich habe es tatsächlich mit einem riesigen Monstrositäts-Parser erreicht, den ich dann zum zweiten Mal passiere, um den Parse-Baum aufzuräumen. Was ich tun möchte, ist eine saubere Parse von unten nach oben, die Dinge wie die Verwendung von Sets für die Paare (Schlüssel, Wert) enthält, so dass redundante Paare eliminiert werden usw. Während ich es vorher funktionierte, fühle ich nicht Wie ich völlig verstanden habe, warum Pyapsing sich so verhielt, wie es war, also habe ich viel Arbeit um mich herum gemacht, irgendwie gegen den Strich gekämpft.Pyparsing setParseAction Funktion erhält keine Argumente

Derzeit hier ist der Anfang meines „vereinfacht“ Parser:

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 

Derzeit ist der „Grammatik“ Nicht-Terminal ist nur ein Stummel, ich werde schließlich nestbarer Konjunktionen und Disjunktionen zu den Tasten hinzufügen, so dass Durchsuchungen

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

Vorerst aber, ich habe Probleme zu verstehen, wie pyparsing meine setParseAction Funktionen aufruft: wie diese können analysiert werden. Ich weiß, dass es eine gewisse Magie gibt, wenn man bedenkt, wie viele Argumente übergeben werden, aber ich bekomme einen Fehler, bei dem überhaupt keine Argumente an die Funktion übergeben werden. Also zur Zeit, wenn ich es tue:

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

ich diesen Fehler:

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) 

Wie Sie aus der Zurückverfolgungs sehen können, ich bin mit python2.6 und pyparsing 1.5.6

Kann mir jemand einen Einblick geben, warum die Funktion nicht die richtige Anzahl von Argumenten bekommt?

+0

Dieser Code könnte wirklich Kommentare verwenden, um uns Lesern zu helfen. – Paragon

+0

Das meiste davon ist ziemlich Standard-PIPARSING-Code, welchen Teil möchten Sie geklärt haben? – deontologician

+0

Es ist einfach nicht gut lesbar. Ich begann, den Code durchzugehen, aber ich stoppte, weil es zu lange dauert, um den Zweck von Funktionen mit nicht-beschreibenden Namen herauszufinden. – Paragon

Antwort

20

Nun, die neueste Version von setParseAction macht einige zusätzliche Magie, aber leider auf Kosten einiger Entwicklungs Einfachheit. Die Argument-Erkennungslogik in setParseAction beruht nun auf dem Auslösen von Ausnahmen in der Parse-Aktion, bis sie mit der richtigen Anzahl von Argumenten aufgerufen wird, beginnend bei 3 und abwärts abläuft. Danach gibt sie einfach auf und löst die Ausnahme aus sah.

Außer in diesem Fall war die Ausnahme, die von der Analyseaktion herrührt, nicht aufgrund eines Konflikts mit der Argumentliste, sondern ein echter Fehler in Ihrem Code. Um eine bessere Sicht auf das bekommen, legen Sie eine generische try-except in die Parse-Aktion:

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

Und Sie erhalten:

unhashable type: 'set' 

In der Tat, das zweite Element der Liste, von denen Sie das Erstellen des Rückgabe-Sets ist selbst ein Set, ein veränderbarer Container, also nicht für die Aufnahme in ein Set hashbar. Wenn Sie dies ändern, um stattdessen einen Tiefkühlsatz zu verwenden, erhalten Sie:

[set([('hi', frozenset([]))])] 

Warum ist das Frozenset leer?Ich schlage vor, Sie den Speicherort Ihrer Ergebnisse Namen ändern ‚vs‘ zu:

nestedExpr('{','}', content = delimitedList(value))('vs') 

Und nun die zurück analysierten Ergebnisse von ‚hallo in {1,2,3}‘ Parsen sind:

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

das ist etwas, ein Durcheinander, wenn wir diese Linie an der Spitze Ihrer Parse-Aktion fallen, werden Sie sehen, was die verschiedenen genannten Ergebnisse tatsächlich enthalten:

print tokens.dump() 

wir erhalten:

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

So 'vs' zeigt tatsächlich auf eine Liste mit einer Liste. Also wollen wir wahrscheinlich unser Set von tokens.vs[0], nicht tokens.vs bauen. Nun sieh mal unsere analysiert Ergebnisse wie:

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

Einige andere Tipps zur Grammatik:

  • Statt CaselessLiteral, versuchen CaselessKeyword verwenden. Schlüsselwörter sind die bessere Wahl für grammatikalische Schlüsselwörter, da sie inhärent vermeiden, das führende "in" von "innen" als das Schlüsselwort "in" in Ihrer Grammatik zu verwechseln.

  • Nicht sicher, wohin Sie mit zurückkehrenden Sets aus den Parse-Aktionen - für Schlüssel-Wert-Paare, ein Tupel wird wahrscheinlich besser sein, da es die Reihenfolge der Token erhalten wird. Erstellen Sie Ihre Schlüssel und Werte in der After-Parsing-Phase des Programms.

  • Für andere Grammatik-Debugging-Tools, überprüfen Sie setDebug und die traceParseAction Dekorateur.

+2

Vielen Dank! Um zu vermeiden, dass pyparsing versteckt wird, warum die Ausnahme ausgelöst wird, müssen Sie im Allgemeinen eine Ausnahme in Ihren parseAction-Funktionen abfangen. Wäre das genau? – deontologician

+1

Angesichts der neuen Implementierung der Normalisierung von Parse-Action, ja, dies wäre eine gute Richtlinie, im Allgemeinen zu folgen. Es gibt jedoch manchmal, wenn Sie wollen * eine Parser-Aktion, um eine Ausnahme auszulösen, wie wenn Sie einige semantische Validierung durchführen, in diesem Fall sollte Ihr Code eine pyparsing ParseException auslösen. – PaulMcG

+2

Diese hilfreiche Antwort sollte in der Dokumentation der 'setParseAction()' Methode enthalten sein. – cfi

5

Paul hat bereits erklärt, was das eigentliche Problem ist: Die TypeError durch Ihre Parse-Aktion erhöht pyparsing die Automagic Weg herauszufinden, die Anzahl der Argumentation verwechselt Ihre Parse-Aktion erwartet.

Hier ist, was ich diese Art von Verwirrung zu vermeiden: Ein Dekorateur, die jede TypeError durch die dekorierten Funktion geworfen wieder erhöht, wenn die Funktion wieder mit weniger Argumenten aufgerufen wird:

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 

Hier ist, wie es zu benutzen :

from pyparsing import Literal 

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

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

Dies gibt Ihnen:

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 

Co mpare das mit dem traceback, das Sie ohne die @parse_action Dekoration erhalten:

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) 
+0

Das war sehr hilfreich. Danke, dass Sie es geschrieben und gepostet haben. – mhucka

+0

Ich habe 'setParseAction' etwas Code hinzugefügt, um es weniger anfällig für diese Art von TypeError-Verwechslungen zu machen - funktioniert diese Lösung noch mit der neuesten Version von pyparsing? – PaulMcG

+0

@PaulMcGuire: Welche Version wäre das? Das [neueste auf PyPI] (https://pypi.python.org/pypi/pyparsing) und [auf der Homepage] (http://pyparsing.wikispaces.com/News) ist 2.0.3, aber es gibt nichts über ' SetParseAction' in seinem Changelog –