python - 快速评估大量输入值的数学表达式(函数)

标签 python eval abstract-syntax-tree sympy numexpr

下面的问题

和他们各自的答案让我思考如何能够有效地解析由(或多或少受信任的)用户给出的单个数学表达式(按照此答案 https://stackoverflow.com/a/594294/1672565 的一般术语)来处理 20k 到 30k 的输入值来自数据库。我实现了一个快速而粗略的基准测试,以便我可以比较不同的解决方案。

# Runs with Python 3(.4)
import pprint
import time

# This is what I have
userinput_function = '5*(1-(x*0.1))' # String - numbers should be handled as floats
demo_len = 20000 # Parameter for benchmark (20k to 30k in real life)
print_results = False

# Some database, represented by an array of dicts (simplified for this example)

database_xy = []
for a in range(1, demo_len, 1):
    database_xy.append({
        'x':float(a),
        'y_eval':0,
        'y_sympya':0,
        'y_sympyb':0,
        'y_sympyc':0,
        'y_aevala':0,
        'y_aevalb':0,
        'y_aevalc':0,
        'y_numexpr': 0,
        'y_simpleeval':0
        })

# 解决方案 #1:eval [是的,完全不安全]

time_start = time.time()
func = eval("lambda x: " + userinput_function)
for item in database_xy:
    item['y_eval'] = func(item['x'])
time_end = time.time()
if print_results:
    pprint.pprint(database_xy)
print('1 eval: ' + str(round(time_end - time_start, 4)) + ' seconds')

# 解决方案 #2a:sympy - evalf ( http://www.sympy.org )

import sympy
time_start = time.time()
x = sympy.symbols('x')
sympy_function = sympy.sympify(userinput_function)
for item in database_xy:
    item['y_sympya'] = float(sympy_function.evalf(subs={x:item['x']}))
time_end = time.time()
if print_results:
    pprint.pprint(database_xy)
print('2a sympy: ' + str(round(time_end - time_start, 4)) + ' seconds')

# 解决方案 #2b:sympy - lambdify ( http://www.sympy.org )

from sympy.utilities.lambdify import lambdify
import sympy
import numpy
time_start = time.time()
sympy_functionb = sympy.sympify(userinput_function)
func = lambdify(x, sympy_functionb, 'numpy') # returns a numpy-ready function
xx = numpy.zeros(len(database_xy))
for index, item in enumerate(database_xy):
    xx[index] = item['x']
yy = func(xx)
for index, item in enumerate(database_xy):
    item['y_sympyb'] = yy[index]
time_end = time.time()
if print_results:
    pprint.pprint(database_xy)
print('2b sympy: ' + str(round(time_end - time_start, 4)) + ' seconds')

# Solution #2c:sympy - lambdify with numexpr [and numpy] ( http://www.sympy.org )

from sympy.utilities.lambdify import lambdify
import sympy
import numpy
import numexpr
time_start = time.time()
sympy_functionb = sympy.sympify(userinput_function)
func = lambdify(x, sympy_functionb, 'numexpr') # returns a numpy-ready function
xx = numpy.zeros(len(database_xy))
for index, item in enumerate(database_xy):
    xx[index] = item['x']
yy = func(xx)
for index, item in enumerate(database_xy):
    item['y_sympyc'] = yy[index]
time_end = time.time()
if print_results:
    pprint.pprint(database_xy)
print('2c sympy: ' + str(round(time_end - time_start, 4)) + ' seconds')

# 解决方案 #3a:asteval [基于 ast] - 使用字符串魔法 ( http://newville.github.io/asteval/index.html )

from asteval import Interpreter
aevala = Interpreter()
time_start = time.time()
aevala('def func(x):\n\treturn ' + userinput_function)
for item in database_xy:
    item['y_aevala'] = aevala('func(' + str(item['x']) + ')')
time_end = time.time()
if print_results:
    pprint.pprint(database_xy)
print('3a aeval: ' + str(round(time_end - time_start, 4)) + ' seconds')

# 解决方案 #3b (M Newville):asteval [基于 ast] - 解析并运行 ( http://newville.github.io/asteval/index.html )

from asteval import Interpreter
aevalb = Interpreter()
time_start = time.time()
exprb = aevalb.parse(userinput_function)
for item in database_xy:
    aevalb.symtable['x'] = item['x']
    item['y_aevalb'] = aevalb.run(exprb)
time_end = time.time()
print('3b aeval: ' + str(round(time_end - time_start, 4)) + ' seconds')

# Solution #3c (M Newville):asteval [基于 ast] - 使用 numpy ( http://newville.github.io/asteval/index.html ) 解析和运行

from asteval import Interpreter
import numpy
aevalc = Interpreter()
time_start = time.time()
exprc = aevalc.parse(userinput_function)
x = numpy.array([item['x'] for item in database_xy])
aevalc.symtable['x'] = x
y = aevalc.run(exprc)
for index, item in enumerate(database_xy):
    item['y_aevalc'] = y[index]
time_end = time.time()
print('3c aeval: ' + str(round(time_end - time_start, 4)) + ' seconds')

# 解决方案 #4:simpleeval [基于 ast] ( https://github.com/danthedeckie/simpleeval )

from simpleeval import simple_eval
time_start = time.time()
for item in database_xy:
    item['y_simpleeval'] = simple_eval(userinput_function, names={'x': item['x']})
time_end = time.time()
if print_results:
    pprint.pprint(database_xy)
print('4 simpleeval: ' + str(round(time_end - time_start, 4)) + ' seconds')

# 解决方案 #5 numexpr [和 numpy] ( https://github.com/pydata/numexpr )

import numpy
import numexpr
time_start = time.time()
x = numpy.zeros(len(database_xy))
for index, item in enumerate(database_xy):
    x[index] = item['x']
y = numexpr.evaluate(userinput_function)
for index, item in enumerate(database_xy):
    item['y_numexpr'] = y[index]
time_end = time.time()
if print_results:
    pprint.pprint(database_xy)
print('5 numexpr: ' + str(round(time_end - time_start, 4)) + ' seconds')

在我的旧测试机器(Python 3.4、Linux 3.11 x86_64、两个内核、1.8GHz)上,我得到以下结果:

1 eval: 0.0185 seconds
2a sympy: 10.671 seconds
2b sympy: 0.0315 seconds
2c sympy: 0.0348 seconds
3a aeval: 2.8368 seconds
3b aeval: 0.5827 seconds
3c aeval: 0.0246 seconds
4 simpleeval: 1.2363 seconds
5 numexpr: 0.0312 seconds

突出的是 eval 令人难以置信的速度,尽管我不想在现实生活中使用它。第二个最佳解决方案似乎是 numexpr,它依赖于 numpy - 我想避免的依赖性,尽管这不是硬性要求。次要的是 simpleeval,它是围绕 ast 构建的。 aeval 是另一个基于 ast 的解决方案,它的缺点是我必须首先将每个浮点输入值转换为字符串,而我找不到解决方法。 sympy 最初是我最喜欢的,因为它提供了最灵活且显然最安全的解决方案,但它最终以与倒数第二个解决方案的一些令人印象深刻的差距结束。

更新 1:使用 sympy 有一种更快的方法。请参见解决方案 2b。它几乎与 numexpr 一样好,但我不确定 sympy 是否真的在内部使用它。

更新 2:sympy 实现现在使用 sympify 而不是 simplify(由其首席开发人员推荐) , asmeurer - 谢谢)。它不会使用 numexpr 除非明确要求这样做(请参阅解决方案 2c)。我还添加了两个基于 asteval 的明显更快的解决方案(感谢 M Newville)。


我有哪些选择可以进一步加快任何相对安全的解决方案的速度?例如,是否有其他安全的(-ish)方法直接使用 ast?

最佳答案

我用过 C++ ExprTK图书馆过去取得了巨大的成功。 Here是其他 C++ 解析器(例如 Muparser、MathExpr、ATMSP 等...)中的基准速度测试,ExprTK 名列前茅。

ExprTK 有一个名为 cexprtk 的 Python 包装器我用过并且发现速度非常快。您可以只编译一次数学表达式,然后根据需要多次计算此序列化表达式。下面是一个使用 cexprtkuserinput_function 的简单示例代码:

import cexprtk
import time

userinput_function = '5*(1-(x*0.1))' # String - numbers should be handled as floats
demo_len = 20000 # Parameter for benchmark (20k to 30k in real life)

time_start = time.time()
x = 1

st = cexprtk.Symbol_Table({"x":x}, add_constants = True) # Setup the symbol table
Expr = cexprtk.Expression(userinput_function, st) # Apply the symbol table to the userinput_function

for x in range(0,demo_len,1):
    st.variables['x'] = x # Update the symbol table with the new x value
    Expr() # evaluate expression
time_end = time.time()

print('1 cexprtk: ' + str(round(time_end - time_start, 4)) + ' seconds')

在我的机器上(Linux,双核,2.5GHz),演示长度为 20000,这在 0.0202 秒内完成。

对于长度为 2,000,000 的演示,cexprtk 在 1.23 秒内完成。

关于python - 快速评估大量输入值的数学表达式(函数),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34106484/

相关文章:

java - eclipse JDT AST : How to write generated AST to java file?

python - 在 Python,pandas 中,当我将列从六进制转换为十进制时如何忽略 Python 中的无效值?

python - 无法在同一任务的辅助函数中访问 Celery 任务的任务 ID?

python - 如何在ForeignKey字段中使用Django中自动创建的隐式模型类?

python - 使用 map 中的 eval() 执行 python 代码行

javascript - 评估 Json - 嵌套的 JSON 路径作为字符串提供

java - 如何使用 ANTLR 获取 Python 中每个函数的 ast

python - Visual Studio Code 中调试和运行模式下使用的不同 python 环境

jQuery 的 getScript - 将文件包含到主范围中?

c# - 开发抽象语法树