【问题标题】:Convert custom formula to python function [duplicate]将自定义公式转换为python函数[重复]
【发布时间】:2020-02-27 06:12:41
【问题描述】:

考虑我们有以下输入

formula = "(([foo] + [bar]) - ([baz]/2) )"

function_mapping = {
                   "foo" : FooFunction,
                   "bar" : BarFunction,
                   "baz" : BazFunction,  
                  }

是否有任何 python 库可以让我解析公式并将其转换为 python 函数表示。

例如。

converted_formula = ((FooFunction() + BarFunction() - (BazFunction()/2))

我目前正在研究类似的东西

In [11]: ast = compiler.parse(formula)

In [12]: ast
Out[12]: Module(None, Stmt([Discard(Sub((Add((List([Name('foo')]), List([Name('bar')]))), Div((List([Name('baz')]), Const(2))))))]))

然后进一步处理这个 ast 树。

您知道任何更清洁的替代解决方案吗? 非常感谢任何帮助或见解!

【问题讨论】:

  • 使用compiler.parse()的一个潜在问题是它根据Python语法进行解析,这就是为什么它将公式中的[foo]变成List([Name('foo')])的原因。公式中使用的语法是什么?
  • @martineau 确实,公式结构存在问题,因为它与 python List 类型冲突。我定义了公式的语法,因此可以给出类似In [18]: formula = "((foo + bar) - (baz/2) )" In [19]: ast = compiler.parse(formula) In [20]: ast Out[20]: Module(None, Stmt([Discard(Sub((Add((Name('foo'), Name('bar'))), Div((Name('baz'), Const(2))))))]))
  • 您可以通过“简单地”进行文本替换来回避compiler.parse() 的语法问题,如下面的answer 所示。也就是说,最好定义公式表达式的语法,这样它就不会与现有的 Python 语法冲突。例如,代替re 模块,可以使用string.Template 替换$ 语法来做你想做的事情,这可能更容易理解和实现。

标签: python parsing abstract-syntax-tree dsl


【解决方案1】:

您可以使用re 模块通过正则表达式模式匹配和相对简单的文本替换来做您想做的事情。

import re

alias_pattern = re.compile(r'''(?:\[(\w+)\])''')

def mapper(mat):
    func_alias = mat.group(1)
    function = function_alias_mapping.get(func_alias)
    if not function:
        raise NameError(func_alias)
    return function.__name__ + '()'

# must be defined before anything can be mapped to them
def FooFunction(): return 15
def BarFunction(): return 30
def BazFunction(): return 6

function_alias_mapping =  dict(foo=FooFunction, bar=BarFunction, baz=BazFunction)
formula = "(([foo] + [bar]) - ([baz]/2))"  # Custom formula.

converted_formula = re.sub(alias_pattern, mapper, formula)
print('converted_formula = "{}"'.format(converted_formula))

# define contexts and function in which to evalute the formula expression
global_context = dict(FooFunction=FooFunction,
                      BarFunction=BarFunction,
                      BazFunction=BazFunction)
local_context = {'__builtins__': None}

function = lambda: eval(converted_formula, global_context, local_context)
print('answer = {}'.format(function()))  # call function

输出:

converted_formula = "((FooFunction() + BarFunction()) - (BazFunction()/2))"
answer = 42

【讨论】:

  • 这很好用!谢谢!由于担心安全隐患,我开始放弃使用eval。只要我彻底验证输入,你认为这对eval 来说是一个有效的用例吗?
  • “相当好”是什么意思?如果您采取某些预防措施,eval 是可以的 - 请参阅更新的答案。
  • 我想我想说“完美运行!” :) 感谢更新!将确保我在使用 eval 之前采取必要的预防措施。
【解决方案2】:

您可以使用所谓的字符串格式化来完成此操作。

function_mapping = {
                   "foo" : FooFunction(),
                   "bar" : BarFunction(),
                   "baz" : BazFunction(),  
                  }

formula = "(({foo} + {bar}) - ({baz}/2) )".format( **function_mapping )

会给你((FooFunction() + BarFunction() - (BazFunction()/2))的结果

但我相信这些函数会在加载模块时执行,所以也许更好的解决方案是

function_mapping = {
                   "foo" : "FooFunction",
                   "bar" : "BarFunction",
                   "baz" : "BazFunction",  
                  }

formula = "(({foo}() + {bar}()) - ({baz}()/2) )".format( **function_mapping )

这将为您提供字符串'((FooFunction() + BarFunction() - (BazFunction()/2))',然后您可以随时使用eval 函数执行该字符串。

【讨论】:

  • 第二个示例中缺少函数的括号。其他细节:如果一个函数在公式中出现多次,它将被多次调用。 LRU,脏标志或缓存在这里很有用。
  • @aluriak 我不知道有一种内置方法可以为您自动记忆功能。这太酷了。而且我已经更新了我的答案以包括括号。感谢您指出这一点。
  • "这会给你字符串 '((FooFunction() + BarFunction() - (BazFunction()/2))'",实际上没有......这是完全错误的...... .
  • @Mr.Me :Daniel 是对的,你得到的字符串将类似于 In [10]: formula Out[10]: '((<function FooFunction at 0x109749140>() + <function BarFunction at 0x1097491b8>()) - (<function BazFunction at 0x1097b5a28>()/2) )' 。不过谢谢!
  • 我真的应该在发布代码之前对其进行全面测试。将函数名称放在字符串中将解决此问题。如果您无法对其进行硬编码,则有一些方法可以获取任何函数的字符串名称。有关如何执行此操作的信息,请参阅 stackoverflow.com/questions/251464/…
【解决方案3】:

如果您稍微更改公式中使用的语法,(另一种)方法(正如我在comment 中提到的那样)将使用string.Template 替换。

出于好奇,我决定找出这种其他方法是否可行——因此能够提出更好的答案,因为它不仅比我的 other 更简单,而且更灵活一点从某种意义上说,向被调用的函数添加参数很容易,如下面的评论中所述。

from string import Template

def FooFunction(): return 15
def BarFunction(): return 30
def BazFunction(): return 6

formula = "(($foo + $bar) - ($baz/2))"

function_mapping = dict(foo='FooFunction()',  # note these calls could have args
                        bar='BarFunction()',
                        baz='BazFunction()')

converted_formula = Template(formula).substitute(function_mapping)
print('converted_formula = "{}"'.format(converted_formula))

# define contexts in which to evalute the expression
global_context = dict(FooFunction=FooFunction,
                      BarFunction=BarFunction,
                      BazFunction=BazFunction)
local_context = dict(__builtins__=None)
function = lambda: eval(converted_formula, global_context, local_context)

answer = function()  # call it
print('answer = {}'.format(answer))

作为最后一点,请注意string.Template 支持不同类型的高级用法,这将允许您进一步微调表达式语法——因为它在内部使用re 模块(比我在原始答案中所做的更复杂)。

对于映射函数的所有返回值都可以表示为 Python 字面量(如数字)的情况,并且不会仅仅因为它们产生的副作用而被调用,您可以进行以下修改以有效缓存(又名memoize) 结果:

function_cache = dict(foo=FooFunction(),  # calls and caches function results
                      bar=BarFunction(),
                      baz=BazFunction())

def evaluate(formula):
    print('formula = {!r}'.format(formula))
    converted_formula = Template(formula).substitute(function_cache)
    print('converted_formula = "{}"'.format(converted_formula))
    return eval(converted_formula, global_context, local_context)

print('evaluate(formula) = {}'.format(evaluate(formula)))

输出:

formula = '(($foo + $bar) - ($baz/2))'
converted_formula = "((15 + 30) - (6/2))"
evaluate(formula) = 42

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-23
    • 1970-01-01
    • 2021-11-01
    • 1970-01-01
    相关资源
    最近更新 更多