使用pyparsing为DSL评估嵌套变量

时间:2012-12-10 23:33:15

标签: python dsl pyparsing

我一直致力于通过pyparsing构建我的DSL并取得了很好的进展。我的第一个里程碑是评估包含算术运算符,数据库字段引用和一组函数(Avg,Stdev等)的表达式。另外,我实现了对变量的表达式赋值,以便能够以模块化方式构建复杂表达式。到目前为止一切都很好。

在尝试将变量计算函数作为参数时,我现在遇到了下一个主要问题。具体来说,我的数据库引用(执行计算的构建块)需要指定Person作为查询的维度。当它们包含在函数中时,我不知道强制重新评估分配给这些变量的表达式的最佳方法。有问题的具体例子:

1) CustomAvg = Avg[Height] + Avg[Weight]
2) Avg[CustomAvg]

评估语句2在People列表中无法正常工作,因为CustomAvg正在被解析为常量值。

在这些场景中,我有一个人员列表,我迭代计算CustomAvg的组件。但是,当我评估Avg [CustomAvg]时,CustomAvg的值来自我的变量查找字典而不是被评估,因此我有效地迭代一个常量值。在我的评估中引入“意识”的最佳方法是什么,以便在函数中用作参数的变量被重新评估而不是从查找表中获取?以下是简化的相关代码:

class EvalConstant(object):
    var_ = {}
    def __init__(self, tokens):
        self.value = tokens[0]

    def eval(self):
        v = self.value
        if self.var_.has_key(v):
            return self.var_[v]
        else:
            return float(v)

class EvalDBref(object):
    person_ = None
    def __init__(self, tokens):
        self.value = tokens[0]

    def eval(self):
        v = self.value
        fieldRef = v.split(':')
        source = fieldRef[0]
        field = fieldRef[1] 
        rec = db[source].find_one({'Name' : self.person_}, { '_id' : 0, field : 1})
        return rec[field]

class EvalFunction(object):
    pop_ = {}
    def __init__(self, tokens):
        self.func_ = tokens.funcname
        self.field_ = tokens.arg
        self.pop_ = POPULATION

    def eval(self):
        v = self.field_.value
        fieldRef = v.split(':')
        source = fieldRef[0]
        field = fieldRef[1]

        val = self.field_.eval()

        if self.func_ == 'ZS':
            # If using zscore then fetch the field aggregates from stats
            rec = db['Stats'].find_one({'_id' : field})   
            stdev = rec['value']['stddev']          
            avg = rec['value']['avg']
            return (val - avg)/stdev
        elif self.func_ == 'Ptile':
            recs = list(db[source].find({'Name' : { '$in' : self.pop_}},{'_id' : 0, field : 1}))
            recs = [r[field] for r in recs]
            return percentileofscore(recs, val)

def assign_var(tokens):
    ev = tokens.varvalue.eval()
    EvalConstant.var_[tokens.varname] = ev

#--------------------
expr = Forward()
chars = Word(alphanums + "_-/") 
integer = Word(nums)
real = Combine(Word(nums) + "." + Word(nums))
var = Word(alphas)

assign = var("varname") + "=" + expr("varvalue")
assign.setParseAction(assign_var)

dbRef = Combine(chars + OneOrMore(":") + chars)
dbRef.setParseAction(EvalDBref)

funcNames = Keyword("ZS") | Keyword("Avg") | Keyword("Stdev")
functionCall = funcNames("funcname") + "[" + expr("arg") + "]"
functionCall.setParseAction(EvalFunction)

operand =  dbRef | functionCall | (real | integer| var).setParseAction(EvalConstant) 

signop = oneOf('+ -')
multop = oneOf('* /')
plusop = oneOf('+ -')

expr << operatorPrecedence(operand,
   [
    (signop, 1, opAssoc.RIGHT, EvalSignOp),
    (multop, 2, opAssoc.LEFT, EvalMultOp),
    (plusop, 2, opAssoc.LEFT, EvalAddOp),
   ])

EvalDBref.person_ = ‘John Smith’
ret = (assign | expr).parseString(line)[0]
str(ret.eval())

2 个答案:

答案 0 :(得分:1)

所以在这个表达式中:

CustomAvg = Avg[Height] + Avg[Weight]
应该立即评估

HeightWeight,但是CustomAvg应该在将来的某个时间进行评估?这听起来更像是函数或可调用的定义,而不是新常量。我认为您所要做的就是改变assign_var中发生的事情:

def assign_var(tokens):
    # ev = tokens.varvalue.eval()
    # EvalConstant.var_[tokens.varname] = ev
    EvalConstant.var_[tokens.varname] = tokens.varvalue

现在每个赋值变量都不是常量值,而是eval'able表达式,类似于在Python中创建lambda。然后EvalConstant.eval必须检测它是否只能传回一个值,或者值本身是否需要被评估:

def eval(self):
    v = self.value
    if v in self.var_:  # has_key is deprecated Python, use 'in'
        varval = self.var_[v]
        return varval.eval() if hasattr(varval,'eval') else varval
    else:
        return float(v)

如果您不总是希望这种情况发生,那么我认为您可能需要一些新的语法来区分何时分配常量而不是定义什么本质上是lambda,可能是这样的:

CustomAvg = Avg[Height] + Avg[Weight]    # store as a constant
CustomAvg *= Avg[Height] + Avg[Weight]   # store as a callable

并将assign更改为:

assign = var("varname") + oneOf("= *=")("assign_op") + expr("varvalue")

然后assign_var成为:

def assign_var(tokens):
    if tokens.assign_op == '*=':
        # store expression to be eval'ed later
        EvalConstant.var_[tokens.varname] = tokens.varvalue
    else:
        # eval now and save result
        EvalConstant.var_[tokens.varname] = tokens.varvalue.eval()

答案 1 :(得分:0)

我认为你的问题是范围界定。函数的参数通常被认为与本地函数的范围相同。因此语句CustomAvg = Avg[Height] + Avg[Weight] Avg[CustomAvg]应该将本地CustomAvg当前值推送到堆栈,计算表达式,然后将结果存储到CustomAvg中。 (或者,如果您使用名称/值的Pythonic视图,请将名称CustomAvg设置为指向结果。)

由于赋值在eval堆栈上推送很久后发生,因此不应该有任何歧义。