Для конкретного приложения я создаю графический интерфейс для управления некоторыми данными (внутренне: одномерные массивы numpy) и рисую их график.
Конечный пользователь может выбрать в пользовательском интерфейсе возможность построения различных серий a, b, c.
Теперь мне также нужно разрешить «пользовательскую комбинацию» a, b, c. Точнее, пользователь (который не знает Python/Numpy, но может выучить несколько ключевых слов) должен ввести в текстовое поле графического интерфейса «формулу», а затем моя программа должна транскрибировать ее в реальный код numpy (вероятно, с использованием eval(...), здесь мало проблем с безопасностью, поскольку конечный пользователь является единственным пользователем), и отобразите данные.
Примеры ввода конечного пользователя:
a * 3 + 1.234 * c - da + b.roll(2)a + b / b.max() * a.max()
Например, разрешенный синтаксис: базовая арифметика (+ * - / и круглые скобки), числа с плавающей запятой, a.max() и a.roll(3) для сдвига массивов.
Вопрос: есть ли внутри Numpy или Scipy функция, позволяющая интерпретировать комбинации массивов с помощью базовой арифметической грамматики?
Я бы посмотрел на модуль «ast» абстрактного синтаксического дерева в Python и настроил проверку грамматики в соответствии с вашими требованиями.
Вы говорите, что можете принять a.max(), но ваш пример ввода пользователя показывает max(a). Какой из них действителен?
Рассматривали ли вы возможность использования Jupyter Notebook, предоставив базовый шаблон и добавив необходимый графический интерфейс с помощью ipywidgets. Таким образом пользователь получит доступ к стандартной библиотеке Numpy.
@igrinis Нет, Jupyter / ipywidgets выходит за рамки проекта.






Для алгебраической части вы можете использовать библиотеку numexpr. Например, следующий фрагмент будет работать:
import numpy as np
import numexpr as ne
a = np.random.rand(10)
b = np.random.rand(10)
c = np.random.rand(10)
d = np.random.rand(10)
ne.evaluate("a * 3 + 1.234 * c - d")
К сожалению, библиотека не охватывает сразу два других случая, но этого можно легко добиться с помощью анализа строк. Окончательная версия со всеми вашими функциями может выглядеть так:
import numpy as np
import numexpr as ne
import re
a = np.random.rand(10)
b = np.random.rand(10)
c = np.random.rand(10)
d = np.random.rand(10)
def expression_eval(
expression:str, a:np.array, b: np.array, c:np.array, d:np.array
) -> np.array:
#Snippet to manage max values:
a_max = a.max()
b_max = b.max()
c_max = c.max()
d_max = d.max()
for label in ["a", "b", "c", "d"]:
expression = expression.replace(f"{label}.max()", f"{label}_max")
#Snippet to manage rolling windows:
pattern = r'(\w)\.roll\((\d+)\)'
matches = re.findall(pattern, expression)
if matches: roll_results = [(match[0], int(match[1])) for match in matches]
else: roll_results = []
rolls = {}
for arr, window in roll_results:
expression = expression.replace(f"{arr}.roll({window})", f"{arr}_roll_{window}")
rolls[f"{arr}_roll_{window}"] = np.concatenate([
vars()[arr][window:],
np.zeros(window)
])
return ne.evaluate(expression, global_dict=rolls)
#Evaluation:
expression_1 = "a * 3 + 1.234 * c - d"
expression_2 = "a + b / b.max() * a.max()"
expression_3 = "a + b.roll(3) + c.roll(2) + d.roll(4)"
print(f"{expression_1}\n{expression_eval(expression_1, a, b, c, d)}\n")
print(f"{expression_2}\n{expression_eval(expression_2, a, b, c, d)}\n")
print(f"{expression_3}\n{expression_eval(expression_3, a, b, c, d)}\n")
По сути, мы заменяем каждую функцию переменной ее вычисленным значением перед оценкой алгебраического выражения. Обратите внимание, что для скользящих окон мы можем использовать словарь с более динамичным подходом, чтобы адаптироваться ко многим возможностям скользящих окон.
@cards спросил в комментариях, действительно ли этот код может обрабатывать некоторые вложенные выражения. Ответ: не может. Однако мы можем расширить этот базовый прототип для обработки более сложных выражений, таких как выражение_4. Библиотека numexpr уже обрабатывает вложенность алгебраических выражений, и мы можем разрешить некоторые дополнительные возможности вложенности, такие как получение максимального, минимального и рулонов пользовательских выражений путем предварительного вычисления вложенных выражений, замены их в конечном выражении и передачи значения тега в окончательная оценка.
import numpy as np
import numexpr as ne
import re
a = np.random.rand(10)
b = np.random.rand(10)
c = np.random.rand(10)
d = np.random.rand(10)
def expression_eval(
expression:str, a:np.array, b: np.array, c:np.array, d:np.array
) -> np.array:
variable_dict = {"a":a, "b":b, "c":c, "d":d}
#Snippet to evaluate inner algebraic expressions:
pattern = r'\(.*?\)(?:\.max\(\)|\.min\(\)|\.roll\(.*\))'
matches = list(set(re.findall(pattern, expression)))
for expr_ind, match in enumerate(matches):
expression = re.sub(re.escape(match), f"expr_{expr_ind}", expression)
variable_dict[f"expr_{expr_ind}"] = ne.evaluate(expr_ind)
#Snippet to manage max values:
pattern = r'(\w)\.max\(\)'
matches = re.findall(pattern, expression)
for match in matches:
expression = expression.replace(f"{match}.max()", f"{match}_max")
variable_dict[f"{match}_max"] = variable_dict[match].max()
#Snippet to manage min values:
pattern = r'(\w)\.min\(\)'
matches = re.findall(pattern, expression)
for match in matches:
expression = expression.replace(f"{match}.max()", f"{match}_max")
variable_dict[f"{match}_max"] = variable_dict[match].max()
#Snippet to manage rolling windows:
pattern = r'(\w)\.roll\((\d+)\)'
matches = re.findall(pattern, expression)
if matches: roll_results = [(match[0], int(match[1])) for match in matches]
else: roll_results = []
for arr, window in roll_results:
expression = expression.replace(f"{arr}.roll({window})", f"{arr}_roll_{window}")
variable_dict[f"{arr}_roll_{window}"] = np.concatenate([
vars()[arr][window:],
np.zeros(window)
])
return ne.evaluate(expression, global_dict=variable_dict)
#Evaluation:
expression_1 = "a * 3 + 1.234 * c - d"
expression_2 = "a + b / b.max() * a.max()"
expression_3 = "a + b.roll(3) + c.roll(2) + d.roll(4)"
expression_4 = "((a+b)**3).min()) + ((c-d)*5).roll(3)"
print(f"{expression_1}\n{expression_eval(expression_1, a, b, c, d)}\n")
print(f"{expression_2}\n{expression_eval(expression_2, a, b, c, d)}\n")
print(f"{expression_3}\n{expression_eval(expression_3, a, b, c, d)}\n")
print(f"{expression_4}\n{expression_eval(expression_3, a, b, c, d)}\n")
Спасибо за ваш ответ! В чем в этом контексте преимущество использования numexpr.evaluate("a * 3 + 1.234 * c - d") перед eval("a * 3 + 1.234 * c - d")?
Обрабатывает ли он вложенные выражения? вроде как ((a+b)**3).max())?
@Basj оценивать буквальные выражения от третьих лиц рискованно с точки зрения безопасности. Вот блог realpython.com/python-eval-function/… , в котором подробно объясняется проблема. Вы разрешаете внешнему пользователю потенциально внедрить вредоносный код (злонамеренно или по незнанию). Что-то вроде вставки «__import__('subprocess').getoutput('rm –rf *')» (linux) или аналогичного в powershell позволяет удалить все ваши файлы. Прежде чем выражение будет выполнено в eval, необходим тщательный анализ. Использование numexpr ограничивает использование пользователем только алгебраических функций.
Как насчет библиотеки numexpr? В стандартной библиотеке также есть
eval().