следующих вопросовОценка математического выражения (функция) для большого числа входных значений быстрых
- Evaluating a mathematical expression in a string
- Equation parsing in Python
- Safe way to parse user-supplied mathematical formula in Python
- Evaluate math equations from unsafe user input in Python
и их соответствующие ответы заставляют меня думать, как я могу разобрать одно математическое выражение (в общих чертах в соответствии с этими ответами https://stackoverflow.com/a/594294/1672565), которое дает (более или менее доверенный) пользователь эффективно для входных значений от 20 до 30 тыс., Поступающих из базы данных. Я реализовал быстрый и грязный тест, чтобы сравнить различные решения.
# 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')
# Решение # 2с: SymPy - lambdify с numexpr [и 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 [на основе аст] - шпагатом магии (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): аст Eval [на основе аст] - синтаксический & пробег (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')
# Решение # 3в (M Newville): asteval [на основе аст] - синтаксический & бег с 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 x86_64 3,11, два ядра, 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, другое решение, основанное на астрах, страдает тем, что мне нужно преобразовать каждое значение входного значения float в строку сначала, вокруг которой я не мог найти способ. sympy изначально был моим любимым, потому что он предлагает наиболее гибкое и, по-видимому, безопасное решение, но в итоге он оказался последним с впечатляющим расстоянием до второго до последнего решения.
Обновление 1: Существует более быстрый подход, используя sympy. См. Решение 2b. Это почти так же хорошо, как numexpr, хотя я не уверен, действительно ли sympy фактически использует его внутри.
Update 2: В SymPy реализации теперь используют sympify вместо упростить (в соответствии с рекомендациями его ведущим разработчиком, asmeurer - спасибо). Он не использует numexpr, если это явно не предлагается сделать (см. Решение 2c). Я также добавил два значительно более быстрых решения на основе asteval (спасибо M Newville).
Какие у меня есть возможности для ускорения любых относительно безопасных решений? Существуют ли другие, безопасные (-ий) подходы, например, с использованием ast?
Поскольку вы упомянули, что это происходит из базы данных, рассмотрели ли вы возможность реализации функции в SQL-запросе, чтобы увидеть, как она работает? Таким образом, вместо того, чтобы пытаться передать 20k + входы функции через сеть (медленно), вы просто передаете один результат сценарию? – ray
@ray Интересная идея, но в моем случае это всего лишь локальная mongodb, работающая на той же машине, что и приложение, доступное ей. Однако это может измениться в будущем. –
Но вам все равно придется пройти через сетевой стек, чтобы получить доступ к вашему сценарию и базе данных, даже если он работает локально (то есть localhost) – ray