контекст в pyparsing действиях разбора кроме глобалов - PullRequest
4 голосов
/ 01 января 2012

Я бы хотел иметь возможность анализировать два (или любое количество) выражений, каждое из которых имеет собственный набор определений переменных или другой контекст.

Кажется, нет очевидного способа связать контекст с конкретным вызовом pyparsing.ParseExpression.parseString(). Наиболее естественным способом, по-видимому, является использование метода экземпляра некоторого класса в качестве действий разбора. Проблема этого подхода состоит в том, что грамматика должна быть переопределена для каждого контекста синтаксического анализа (например, в классе __init__), что кажется ужасно неэффективным.

Использование pyparsing.ParseExpression.copy() в правилах не помогает; отдельные выражения хорошо клонируются, но подвыражения, из которых они составлены, не обновляются никаким очевидным способом, и поэтому ни одно из действий разбора любого вложенного выражения не вызывается.

Единственный другой способ получить этот эффект - это определить грамматику, которая возвращает абстрактное дерево синтаксического анализа без контекста, и затем обработать его на втором шаге. Это кажется неудобным даже для простых грамматик: было бы неплохо просто вызвать исключение в тот момент, когда используется нераспознанное имя, и оно все равно не будет анализировать языки, такие как C, которые на самом деле требуют контекста того, что было до того, чтобы узнать, какое правило соответствует.

Есть ли другой способ внедрения контекста (конечно, без использования глобальной переменной) в действия разбора выражений pyparsing?

Ответы [ 4 ]

3 голосов
/ 08 апреля 2016

Немного поздно, но поиск в Google pyparsing reentrancy показывает эту тему, поэтому мой ответ.
Я решил проблему с повторным использованием / повторным входом экземпляра синтаксического анализатора, прикрепив контекст к анализируемой строке.Вы создаете подкласс str, помещаете свой контекст в атрибут нового класса str, передаете его экземпляр в pyparsing и возвращаете контекст в действии.

Python 2.7:

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'}

Этот подход делает сам Parser идеально пригодным для повторного использования и повторного ввода.Внутренние элементы pyparsing также являются реентерабельными, если вы не касаетесь статических полей ParserElement.Единственный недостаток заключается в том, что pyparsing сбрасывает свой кэш-пакет при каждом вызове на parseString, но это может быть устранено путем переопределения SpecStr.__hash__ (чтобы сделать его хешируемым, как object, а не str) и некоторой патчей для обезьян.В моем наборе данных это не проблема, так как снижение производительности незначительно, и это даже способствует использованию памяти.

3 голосов
/ 02 января 2012

Я не знаю, обязательно ли это ответит на ваш вопрос, но это один из подходов к настройке синтаксического анализатора для контекста:

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 голос
/ 10 мая 2013

Я столкнулся с этим точным ограничением и использовал 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 просто есть метод синтаксического анализа, который принимает строку и возвращает проанализированное преобразованное представление запроса, которое является поточно-ориентированным и не требует повторного создания грамматики для каждого вызова синтаксического анализа.

1 голос
/ 04 января 2012

Как разрешить действиям разбора быть методами экземпляра, как вы говорите, но только не восстанавливать класс?Вместо этого, когда вы хотите проанализировать другой модуль перевода, сбросьте контекст в том же объекте синтаксического анализатора.

Примерно так:

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» - это ваш контекст.

Конечно, это терпит неудачу, если вы пытаетесь выполнить параллельный синтаксический анализ в разных потоках, но я не вижу, как это могло бы работать в нормальном режиме с общими действиями разбора.

...