Python:字符串评估的快速函数?

时间:2017-04-08 18:42:11

标签: python string metaprogramming

我可能会解决这个问题,但我很好奇是否可以在Python中完成。

我正在尝试构建一个接受字符串并返回基于该字符串的函数的函数。例如,给定b*exp(a*x)和输入列表['a','b','c']有没有办法在Python中动态创建此函数?

def f_fast(a, b, x):
    return b*np.exp(a*x)

我可以看到如何使用eval创建慢速版本:

np_funcs = {'exp':np.exp, 'sin':np.sin, 'cos':np.cos}

def make_func(s, vars):
    def f(*x):
        d = {e:x[i] for i, e in enumerate(vars)}
        values = dict(d.items() + np_funcs.items())
        return eval(s, {"__builtins__": None}, values)
    return f

s = 'b*exp(a*x)'
f = make_func(s, ['a', 'b', 'x'])

但是这个函数会对每个调用进行字符串评估。我想知道是否有一种方法只在创建时将字符串转换为函数,然后后续调用将很快。

目前这种实施非常缓慢:

x = np.linspace(0,1,10)
print timeit.timeit('f(1,2,x)', "from __main__ import f, x, f_fast", number=10000)
print timeit.timeit('f_fast(1,2,x)', "from __main__ import f, x, f_fast", number=10000)

返回

0.16757759497
0.0262638996569

任何帮助,包括解释为什么不能这样做或为什么这是一个愚蠢的方法,将不胜感激。

提前谢谢。

3 个答案:

答案 0 :(得分:2)

这样做的安全方法是痛苦。您将解析表达式,然后发出一个AST,您可以将其传递给compile()。创建AST如下所示:

import ast
expr = ast.Expression(
    lineno=1,
    body=ast.Lambda(
        args=ast.arguments(
            args=[
                ast.arg(arg='a'),
                ast.arg(arg='b'),
                ast.arg(arg='x'),
            ],
            kwonlyargs=[],
            kw_defaults=[],
            defaults=[],
        ),
        body=ast.BinOp(
            left=ast.Name(id='b', ctx=ast.Load()),
            op=ast.Mult(),
            right=ast.Call(
                func=ast.Attribute(
                    value=ast.Name(id='np', ctx=ast.Load()),
                    attr='exp',
                    ctx=ast.Load(),
                ),
                args=[ast.BinOp(
                    left=ast.Name(id='a', ctx=ast.Load()),
                    op=ast.Mult(),
                    right=ast.Name(id='x', ctx=ast.Load()),
                )],
                keywords=[],
            ),
        ),
    ),
)
ast.fix_missing_locations(expr)

然后,您可以将其转换为优化功能并调用它:

code = compile(expr, 'myinput', 'eval', optimize=1)
func = eval(code, {'np': np})
print(func(1, 2, 3))

创建AST的任务很难。您可以自己创建,如上所述,也可以将标志ast.PyCF_ONLY_AST传递给compile(),然后对树进行消毒......但消毒树的难度不容小觑......

  

您将获得一个字符串(例如来自Web输入字段或其他内容),并且需要能够快速调用该字符串。

请记住,如果您未能正确清理树,则会导致针对您的Web服务器的攻击向量非常容易。

答案 1 :(得分:1)

您可以预编译eval表达式中使用的字符串。请注意下面的代码只是为了说明这个概念,具体的例子可以通过评估lambda来实现(如评论中所示)。

fc = compile('b*np.exp(a*x)', '<string>', 'eval')
def f_faster(a, b, x):
    return eval(fc)

然后:

x = np.linspace(0, 1, 10)
print(timeit.timeit('f_faster(1,2,x)', "from __main__ import f_faster, x, fc", number=10000))
print(timeit.timeit('f_fast(1,2,x)', "from __main__ import f, x, f_fast", number=10000))

给出:

0.0241389274597
0.0203421115875

答案 2 :(得分:1)

另一种方法可能会更快,具体取决于您需要评估表达式 vs 将其从字符串转换为函数的次数。

>>> from sympy import *
>>> from sympy.utilities.lambdify import lambdify
>>> f = lambdify((x,a,b), sympify('b*exp(a*x)'))
>>> f(1,1,1)
2.7182818284590451

sympify接受一个字符串并返回一个对sympy有意义的表达式。请注意,这仍然存在风险,因为AFAIK sympify使用eval。另一方面,使用sympy可以访问一系列符号代数处理工具。

编辑:几乎忘了:此代码提供了一个使用exp库中math版本的函数。使用np中的一个很容易;请参阅lambdify doc。