Быстрая оценка математического выражения (функции) для большого количества входных значений

Следующие вопросы

Оценка математического выражения в строке
Разбор уравнений в Python
Безопасный способ анализа предоставленной пользователем математической формулы в Python
Оценка математических уравнений из небезопасного ввода пользователя в Python

и их соответствующие ответы заставили меня подумать, как я могу разобрать одно математическое выражение (в общих чертах в соответствии с этим ответом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')

# Решение # 2c: sympy - lambdify с numberxpr [и 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 (М Ньювилл): asteval [на основе ast] - parse & run (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')

# Решение # 3c (М Ньювилль): 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 Numberxpr [и 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,8 ГГц) я получаю следующие результаты:

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, который строится вокругаст. aevalДругое решение, основанное на AST, страдает от того факта, что мне нужно сначала преобразовать каждое входное значение с плавающей запятой в строку, вокруг которой я не смог найти выход.SymPy изначально был моим фаворитом, потому что он предлагает самое гибкое и, очевидно, самое безопасное решение, но в итоге оно оказалось последним с некоторым впечатляющим расстоянием до второго до последнего решения.

Обновление 1: Гораздо более быстрый подходSymPy, Смотрите решение 2b. Это почти так же хорошо, какnumexprхотя я не уверенSymPy на самом деле использует его внутри.

Обновление 2:SymPy реализации теперь используютsympify вместоупростить (как рекомендовано его ведущим разработчиком, asmeurer - спасибо). Это не использованиеnumexpr если это явно не требуется (см. решение 2c). Я также добавил два значительно более быстрых решения на основеasteval (спасибо М. Ньювиллу).

Какие варианты у меня есть, чтобы ускорить любое из относительно безопасных решений еще дальше? Существуют ли другие, безопасные (-ish) подходы, например, с непосредственным использованием ast?

Ответы на вопрос(5)

Ваш ответ на вопрос