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

Python: fast functions from string evaluations?

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

我正在尝试构建一个接受字符串的函数和 returns 一个基于该字符串的函数。例如,给定 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)

returns

0.16757759497
0.0262638996569

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

提前致谢。

您可以预编译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

执行此操作的安全方法很痛苦。您将解析表达式,然后发出一个 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() 然后对树进行清理...但是清理树的难度不容小觑...

You are given a string (e.g. from a web input field or something) and need to be able to make fast calls to that string.

请记住,如果您未能正确清理树,将很容易成为针对您的 Web 服务器的攻击媒介。

另一种方法可能会更快,具体取决于您需要计算表达式的次数 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 接受一个字符串和 returns 一个对 sympy 有意义的表达式。请注意,这仍然存在风险,因为据我所知,sympify 使用 eval。另一方面,使用 sympy 确实可以让您访问大量符号代数处理工具。

编辑:差点忘了:此代码提供了一个函数,该函数使用 math 库中的 exp 版本。使用 np 中的那个很容易;请参阅 lambdify 文档。