2012-01-01 8 views
4

私は2つ(または任意の数)の式をそれぞれ独自の変数定義やその他のコンテキストセットで解析できるようにしたいと考えています。グローバルのほかにもpyparsing構文解析のコンテキスト

pyparsing.ParseExpression.parseString()の特定の呼び出しとコンテキストを関連付ける明確な方法はないようです。最も自然な方法は、パースアクションとして、あるクラスのインスタンスメソッドを使用することです。このアプローチの問題は、構文解析コンテキストごとに文法を再定義する必要があります(クラスの__init__など)。これはひどく非効率的です。

pyparsing.ParseExpression.copy()をルールに使用することは役に立ちません。個々の式は正常にクローン化されますが、それらから作成されるサブ式は明白な方法で更新されないため、ネストされた式の解析アクションは呼び出されません。

この効果を得るために考えられる唯一の他の方法は、文脈のない抽象構文解析ツリーを返す文法を定義し、次にそれを第2のステップで処理することです。これは単純な文法でも厄介なようです。認識できない名前が使用された瞬間に例外を発生させるだけでいいですし、Cのような言語を解析することはできません。

pyparsing式の解析アクションに(グローバル変数を使用せずに)コンテキストを注入する別の方法がありますか?

答えて

3

これは必ずしもあなたの質問にお答えします場合、私は知りませんが、それはコンテキストにパーサをカスタマイズするための一つのアプローチです:

from pyparsing import Word, alphas, alphanums, nums, oneOf, ParseFatalException 

var = Word(alphas+'_', alphanums+'_').setName("identifier") 
integer = Word(nums).setName("integer").setParseAction(lambda t:int(t[0])) 
operand = integer | var 

operator = oneOf("+ - * /") 
ops = {'+' : lambda a,b:a+b, 
     '-' : lambda a,b:a-b, 
     '*' : lambda a,b:a*b, 
     '/' : lambda a,b:a/b if b else "inf", 
     } 

binop = operand + operator + operand 

# add parse action that evaluates the binary operator by passing 
# the two operands to the appropriate binary function defined in ops 
binop.setParseAction(lambda t: ops[t[1]](t[0],t[2])) 

# closure to return a context-specific parse action 
def make_var_parseAction(context): 
    def pa(s,l,t): 
     varname = t[0] 
     try: 
      return context[varname] 
     except KeyError: 
      raise ParseFatalException("invalid variable '%s'" % varname) 
    return pa 

def eval_binop(e, **kwargs): 
    var.setParseAction(make_var_parseAction(kwargs)) 
    try: 
     print binop.parseString(e)[0] 
    except Exception as pe: 
     print pe 

eval_binop("m*x", m=100, x=12, b=5) 
eval_binop("z*x", m=100, x=12, b=5) 

プリント

1200 
invalid variable 'z' (at char 0), (line:1, col:1) 
1

はHowabout解析アクションをさせますあなたが言うようにinstancemethodsが、クラスをreinstantiateしないでください?代わりに、別の翻訳単位を解析する場合は、同じパーサーオブジェクト内のコンテキストをリセットします。

このような何か:

from pyparsing import Keyword, Word, OneOrMore, alphas, nums 

class Parser: 
    def __init__(self): 
     ident = Word(alphas) 
     identval = Word(alphas).setParseAction(self.identval_act) 
     numlit = Word(nums).setParseAction(self.numlit_act) 
     expr = identval | numlit 
     letstmt = (Keyword("let") + ident + expr).setParseAction(self.letstmt_act) 
     printstmt = (Keyword("print") + expr).setParseAction(self.printstmt_act) 
     program = OneOrMore(letstmt | printstmt) 

     self.symtab = {} 
     self.grammar = program 

    def identval_act(self, (ident,)): 
     return self.symtab[ident] 
    def numlit_act(self, (numlit,)): 
     return int(numlit) 
    def letstmt_act(self, (_, ident, val)): 
     self.symtab[ident] = val 
    def printstmt_act(self, (_, expr)): 
     print expr 

    def reset(self): 
     self.symtab = {} 

    def parse(self, s): 
     self.grammar.parseString(s) 

P = Parser() 
P.parse("""let foo 10 
print foo 
let bar foo 
print bar 
""") 

print P.symtab 
P.parse("print foo") # context is kept. 

P.reset() 
P.parse("print foo") # but here it is reset and this fails 

この例では "SYMTABは、" あなたのコンテキストです。

異なるスレッドで並列解析をしようとすると、これはうまくいかないものの、共有構文解析アクションで正常に動作する方法はわかりません。

+0

構文解析コンテキストをリセットするのは、私が望まないものです。両方のパースは、別々のスレッドと同様に同時に実行できるはずです。 – SingleNegationElimination

+0

それから、私はパーズアクションが文法と密接に結びついているので、各スレッドの文法のコピーと思います。あなたがthreading.local()を混乱させたくない限り。私の例のようなクラスでは、文法を設定することがパフォーマンス上の大きなヒットになるなら、それらのクラスをプールに入れることができます。しかし、私はそれがそれに値することを疑う。 –

1

私はこの正確な制限に遭遇し、スレッドローカルストレージとしてパーサーコンテキスト情報を添付するためにthreading.local()を使用しました。私の場合は、解析アクション関数内にプッシュされ、ポップされる解析された用語のスタックを保持しますが、明らかに、クラスインスタンスへの参照などを格納するためにも使用できます。

それはやや次のようになります。私の場合は

import threading 

__tls = threading.local() 

def parse_term(t): 
    __tls.stack.append(convert_term(t)) 

def parse_concatenation(t): 
    rhs = __tls.stack.pop() 
    lhs = __tls.stack.pop() 

    __tls.stack.append(convert_concatenation(t, lhs, rhs) 

# parse a string s using grammar EXPR, that has parse actions parse_term and 
# parse_concatenation for the rules that parse expression terms and concatenations 
def parse(s): 
    __tls.stack = [] 

    parse_result = EXPR.parseString(s) 

    return __tls.stack.pop() 

ので、スレッドローカルストレージもののすべて、スタックを設定し、解析アクションと文法自体を外部のパブリックAPIのプッシュされ、外部から誰が何が起こっているのか、それを混乱させることはできません。 APIのどこかで、文字列を受け取り、スレッドセーフであり、すべての解析呼び出しのために文法を再作成する必要がない、解析された変換済み表現のクエリを返す構文解析メソッドがあります。

+0

スレッドローカルストレージは、リエントラントでは役に立ちません。 parseアクションがパーサー(例えば 'ast.literal_eval(" 5 + 5 ")')への再帰呼び出しを引き起こす場合、それは依然として同じスレッドにあります。 – SingleNegationElimination

+0

私はあなたにここに従っているかどうかはわかりませんが、少なくとも私の場合は、パーズアクションがパーサへの再帰呼び出しをしないことを確実に知っています。なぜあなたはそれをしたいのですか? – w0utert

+0

本質的な問題は、この方法でコンテキストを関連付けるには、データ競合の最も一般的なケース(スレッドの安全でない変更)が 'threading.local'で解決されたとしても、 、それはまだ他の用途のためのデータ競争を含んでいる。 – SingleNegationElimination

3

少し遅くなりましたが、グーグルpyparsing reentrancyがこのトピックを示していますので、私の答えです。
私はパースされている文字列にコンテキストを添付することで、パーサインスタンスの再利用/再入可能性に関する問題を解決しました。 サブクラスstr、新しいstrクラスの属性にコンテキストを入れ、 のインスタンスをpyparsingに渡して、アクション内でコンテキストを戻します。

のPython 2.7は、このアプローチはParser自体が完全に再利用可能と再入可能になり

from pyparsing import LineStart, LineEnd, Word, alphas, Optional, Regex, Keyword, OneOrMore 

# subclass str; note that unicode is not handled 
class SpecStr(str): 
    context = None # will be set in spec_string() below 
    # override as pyparsing calls str.expandtabs by default 
    def expandtabs(self, tabs=8): 
     ret = type(self)(super(SpecStr, self).expandtabs(tabs)) 
     ret.context = self.context 
     return ret  

# set context here rather than in the constructor 
# to avoid messing with str.__new__ and super() 
def spec_string(s, context): 
    ret = SpecStr(s) 
    ret.context = context 
    return ret  

class Actor(object): 
    def __init__(self): 
     self.namespace = {} 

    def pair_parsed(self, instring, loc, tok): 
     self.namespace[tok.key] = tok.value 

    def include_parsed(self, instring, loc, tok): 
     # doc = open(tok.filename.strip()).read() # would use this line in real life 
     doc = included_doc # included_doc is defined below 
     parse(doc, self) # <<<<< recursion 

def make_parser(actor_type): 
    def make_action(fun): # expects fun to be an unbound method of Actor 
     def action(instring, loc, tok): 
      if isinstance(instring, SpecStr): 
       return fun(instring.context, instring, loc, tok) 
      return None # None as a result of parse actions means 
      # the tokens has not been changed 

     return action 

    # Sample grammar: a sequence of lines, 
    # each line is either 'key=value' pair or '#include filename' 
    Ident = Word(alphas) 
    RestOfLine = Regex('.*') 
    Pair = (Ident('key') + '=' + 
      RestOfLine('value')).setParseAction(make_action(actor_type.pair_parsed)) 
    Include = (Keyword('#include') + 
       RestOfLine('filename')).setParseAction(make_action(actor_type.include_parsed)) 
    Line = (LineStart() + Optional(Pair | Include) + LineEnd()) 
    Document = OneOrMore(Line) 
    return Document 

Parser = make_parser(Actor) 

def parse(instring, actor=None): 
    if actor is not None: 
     instring = spec_string(instring, actor) 
    return Parser.parseString(instring) 


included_doc = 'parrot=dead' 
main_doc = """\ 
#include included_doc 
ham = None 
spam = ham""" 

# parsing without context is ok 
print 'parsed data:', parse(main_doc) 

actor = Actor() 
parse(main_doc, actor) 
print 'resulting namespace:', actor.namespace 

利回り

['#include', 'included_doc', '\n', 'ham', '=', 'None', '\n', 'spam', '=', 'ham'] 
{'ham': 'None', 'parrot': 'dead', 'spam': 'ham'} 

ParserElementの静的フィールドに触れない限り、pyparsing内部は一般的にリエントラントです。 唯一の欠点はpyparsingparseStringへの呼び出しごとにそのpackratキャッシュをリセットしますが、これはSpecStr.__hash__(それはobject、ないstrのようなハッシュ可能にするため)といくつかのmonkeypatchingをオーバーライド によって解決することができるということです。私のデータセットでは、これはまったく問題ではありません。なぜなら、パフォーマンスヒットは無視できるものであり、これはメモリ使用量をさらに優先させるからです。