Вызовите Python или Lua из С++, чтобы оценить выражение, вычисляя неизвестные переменные только в случае необходимости

У меня есть выражение/формула, такая как

 std::string expr="((A>0) && (B>5 || C > 10))";

Я провел некоторое исследование, и кажется, что если значения A, B, C известны, встраивая Lua или Python в программу на С++, есть функции eval, которые могут заменить A, B и C и возвращать true или false.

Но что происходит, когда я не знаю всех значений? Скажем, что А известно и оно равно -1. Если A равно -1, то формула будет оцениваться как "ложная" независимо от значений B или C.

Можно ли оценить формулу, не зная всех переменных заранее? Например, если A равно 10, имеет смысл искать значение B и повторно оценивать снова. Как мы можем решить эти проблемы? Идеи?

Ответы

Ответ 1

Похоже, у вас есть две проблемы:

  • Вычислить некоторые значения переменных очень дорого, поэтому вы хотите избежать вычисления значений, которые не нужны для оценки выражения; и
  • Ваше выражение существует как строка, составленная во время выполнения, поэтому вы не можете использовать встроенную логику короткого замыкания С++.

Это означает, что вам нужно каким-то образом оценить выражение во время выполнения, и, если это возможно, вы бы хотели воспользоваться схемой короткого замыкания. Python может быть хорошим выбором для этого, как показано в примере ниже.

Существует короткий Python script (evaluate.py), который определяет функцию evaluate(), которая может быть вызвана из вашей программы на C или С++. Функция evaluate() попытается оценить выражение, которое вы ему даете (перевод "& &" и "||" в "и" и "или" при необходимости). Если для этого требуется переменная, которая еще не определена, она будет извлекать значение для этой переменной, вызывая функцию get_var_value(), определенную в программе C/С++ (и затем кэширует значение для последующего использования).

Этот подход будет использовать нормальное поведение короткого замыкания, поэтому он будет запрашивать только значения переменных, необходимые для завершения оценки выражения. Обратите внимание, что это не приведет к изменению выражения для выбора минимального набора переменных, необходимых для его оценки; он просто использует стандартное поведение короткого замыкания.

UPDATE: я добавил пример в конце, который определяет Python script, используя многострочный строковый литерал в файле .cpp. Это может быть полезно, если вы не хотите устанавливать отдельный файл valu.py вместе с исполняемым файлом. Это также немного упрощает инициализацию Python.

Взаимодействие C/Python в приведенных ниже сценариях основано на коде в https://docs.python.org/2/extending/embedding.html и https://docs.python.org/2/c-api/arg.html.

Вот файлы:

оцените .py (Python script)

# load embedded_methods module defined by the parent C program
from embedded_methods import get_var_value

# define a custom dictionary class that calls get_var_value(key) for any missing keys.
class var_dict(dict):
    def __missing__(self, var):
        self[var] = val = get_var_value(var)
        return val

# define a function which can be called by the parent C program
def evaluate(expr):
    # Create a dictionary to use as a namespace for the evaluation (this version 
    # will automatically request missing variables).
    # Move this line up to the module level to retain values between calls.
    namespace = var_dict()

    # convert C-style Boolean operators to Python-style
    py_expr = expr.replace("||", " or ").replace("&&", " and ").replace("  ", " ")

    print('evaluating expression "{}" as "{}"'.format(expr, py_expr))

    # evaluate the expression, retrieving variable values as needed
    return eval(py_expr, namespace)

оцените .c (ваша основная программа, также может быть оценена .cpp, скомпилирована с g++)

// on Mac, compile with gcc -o evaluate evaluate.c -framework Python
#include <Python/Python.h>  // Mac
// #include <Python.h> // non-Mac?

// retain values of argc and argv for equation evaluation
int argc;
char **argv;

/* 
   Calculate the value of a named variable; this is called from the Python 
   script to obtain any values needed to evaluate the expression. 
*/
static PyObject* c_get_var_value(PyObject *self, PyObject *args)
{
    int var_num;
    char *var_name;
    char err_string[100];
    long var_value;
    if(!PyArg_ParseTuple(args, "s:get_var_value", &var_name)) {
        PyErr_SetString(PyExc_ValueError, "Invalid arguments passed to get_var_value()");
        return NULL;
    }
    // change the code below to define your variable values
    // This version just assumes A, B, C are given by argv[2], argv[3], argv[4], etc.
    printf("looking up value of %s: ", var_name);
    var_num = var_name[0]-'A';
    if (strlen(var_name) != 1 || var_num < 0 || var_num >= argc-2) {
        printf("%s\n", "unknown");
        snprintf(
            err_string, sizeof(err_string), 
            "Value requested for unknown variable \"%s\"", var_name
        );
        PyErr_SetString(PyExc_ValueError, err_string);
        return NULL;  // will raise exception in Python
    } else {
        var_value = atoi(argv[2+var_num]);
        printf("%ld\n", var_value);
        return Py_BuildValue("l", var_value);
    }
}

// list of methods to be added to the "embedded_methods" module
static PyMethodDef c_methods[] = {
    {"get_var_value", c_get_var_value, METH_VARARGS, // could use METH_O
     "Retrieve the value for the specified variable."},
    {NULL, NULL, 0, NULL} // sentinel for end of list
};

int main(int ac, char *av[])
{
    PyObject *p_module, *p_evaluate, *p_args, *p_result;
    long result;
    const char* expr;

    // cache and evaluate arguments
    argc = ac;
    argv = av;
    if (argc < 2) {
        fprintf(
            stderr, 
            "Usage: %s \"expr\" A B C ...\n"
            "e.g.,  %s \"((A>0) && (B>5 || C > 10))\" 10 9 -1\n", 
            argv[0], argv[0]
        );
        return 1;
    }
    expr = argv[1];

    // initialize Python
    Py_SetProgramName(argv[0]);
    Py_Initialize();
    // Set system path to include the directory where this executable is stored
    // (to find evaluate.py later)
    PySys_SetArgv(argc, argv);

    // attach custom module with get_var_value() function
    Py_InitModule("embedded_methods", c_methods);

    // Load evaluate.py
    p_module = PyImport_ImportModule("evaluate");
    if (PyErr_Occurred()) { PyErr_Print(); }
    if (p_module == NULL) {
        fprintf(stderr, "unable to load evaluate.py\n");
        return 1;
    }

    // get a reference to the evaluate() function
    p_evaluate = PyObject_GetAttrString(p_module, "evaluate");
    if (!(p_evaluate && PyCallable_Check(p_evaluate))) {
        fprintf(stderr, "Cannot retrieve evaluate() function from evaluate.py module\n");
        return 1;
    }

     /*
        Call the Python evaluate() function with the expression to be evaluated.
        The evaluate() function will call c_get_var_value() to obtain any
        variable values needed to evaluate the expression. It will use 
        caching and normal logical short-circuiting to reduce the number 
        of requests.
     */
    p_args = Py_BuildValue("(s)", expr);
    p_result = PyObject_CallObject(p_evaluate, p_args);
    Py_DECREF(p_args);
    if (PyErr_Occurred()) {
        PyErr_Print();
        return 1;
    }
    result = PyInt_AsLong(p_result);
    Py_DECREF(p_result);

    printf("result was %ld\n", result);

    Py_DECREF(p_evaluate);
    Py_DECREF(p_module);
    return 0;
}

Результаты:

$ evaluate "((A>0) && (B>5 || C > 10))" -1 9 -1
evaluating expression "((A>0) && (B>5 || C > 10))" as "((A>0) and (B>5 or C > 10))"
looking up value of A: -1
result was 0

$ evaluate "((A>0) && (B>5 || C > 10))" 10 9 -1
evaluating expression "((A>0) && (B>5 || C > 10))" as "((A>0) and (B>5 or C > 10))"
looking up value of A: 10
looking up value of B: 9
result was 1

$ evaluate "((A>0) && (B>5 || C > 10))" 10 3 -1
evaluating expression "((A>0) && (B>5 || C > 10))" as "((A>0) and (B>5 or C > 10))"
looking up value of A: 10
looking up value of B: 3
looking up value of C: -1
result was 0

В качестве альтернативы вы можете объединить весь этот код в один .cpp файл, как показано ниже. Это использует многострочный строковый литерал в С++ 11.

Автономная оценка .cpp

// on Mac, compile with g++ evaluate.cpp -o evaluate -std=c++11 -framework Python
#include <Python/Python.h>  // Mac
//#include <Python.h> // non-Mac?

/* 
   Python script to be run in embedded interpreter.
   This defines an evaluate(expr) function which will interpret an expression
   and return the result. If any variable values are needed, it will call the
   get_var_values(var) function defined in the parent C++ program
*/
const char* py_script = R"(
# load embedded_methods module defined by the parent C program
from embedded_methods import get_var_value

# define a custom dictionary class that calls get_var_value(key) for any missing keys.
class var_dict(dict):
    def __missing__(self, var):
        self[var] = val = get_var_value(var)
        return val

# define a function which can be called by the parent C program
def evaluate(expr):
    # Create a dictionary to use as a namespace for the evaluation (this version 
    # will automatically request missing variables).
    # Move this line up to the module level to retain values between calls.
    namespace = var_dict()

    # convert C-style Boolean operators to Python-style
    py_expr = expr.replace("||", " or ").replace("&&", " and ").replace("  ", " ")

    print('evaluating expression "{}" as "{}"'.format(expr, py_expr))

    # evaluate the expression, retrieving variable values as needed
    return eval(py_expr, namespace)
)";

// retain values of argc and argv for equation evaluation
int argc;
char **argv;

/* 
   Calculate the value of a named variable; this is called from the Python 
   script to obtain any values needed to evaluate the expression. 
*/
static PyObject* c_get_var_value(PyObject *self, PyObject *args)
{
    int var_num;
    char *var_name;
    char err_string[100];
    long var_value;
    if(!PyArg_ParseTuple(args, "s:get_var_value", &var_name)) {
        PyErr_SetString(PyExc_ValueError, "Invalid arguments passed to get_var_value()");
        return NULL;
    }
    // change the code below to define your variable values
    // This version just assumes A, B, C are given by argv[2], argv[3], argv[4], etc.
    printf("looking up value of %s: ", var_name);
    var_num = var_name[0]-'A';
    if (strlen(var_name) != 1 || var_num < 0 || var_num >= argc-2) {
        printf("%s\n", "unknown");
        snprintf(
            err_string, sizeof(err_string), 
            "Value requested for unknown variable \"%s\"", var_name
        );
        PyErr_SetString(PyExc_ValueError, err_string);
        return NULL;  // will raise exception in Python
    } else {
        var_value = atoi(argv[2+var_num]);
        printf("%ld\n", var_value);
        return Py_BuildValue("l", var_value);
    }
}

// list of methods to be added to the "embedded_methods" module
static PyMethodDef c_methods[] = {
    {"get_var_value", c_get_var_value, METH_VARARGS, // could use METH_O
     "Retrieve the value for the specified variable."},
    {NULL, NULL, 0, NULL} // sentinel for end of list
};

int main(int ac, char *av[])
{
    PyObject *p_module, *p_evaluate, *p_args, *p_result;
    long result;
    const char* expr;

    // cache and evaluate arguments
    argc = ac;
    argv = av;
    if (argc < 2) {
        fprintf(
            stderr, 
            "Usage: %s \"expr\" A B C ...\n"
            "e.g.,  %s \"((A>0) && (B>5 || C > 10))\" 10 9 -1\n", 
            argv[0], argv[0]
        );
        return 1;
    }
    expr = argv[1];

    // initialize Python
    Py_SetProgramName(argv[0]);
    Py_Initialize();

    // attach custom module with get_var_value() function
    Py_InitModule("embedded_methods", c_methods);

    // run script to define evalute() function
    PyRun_SimpleString(py_script);
    if (PyErr_Occurred()) {
        PyErr_Print(); 
        fprintf(stderr, "%s\n", "unable to run Python script");
        return 1;
    }

    // get a reference to the Python evaluate() function (can be reused later)
    // (note: PyRun_SimpleString creates objects in the __main__ module)
    p_module = PyImport_AddModule("__main__");
    p_evaluate = PyObject_GetAttrString(p_module, "evaluate");
    if (!(p_evaluate && PyCallable_Check(p_evaluate))) {
        fprintf(stderr, "%s\n", "Cannot retrieve evaluate() function from __main__ module");
        return 1;
    }

    /*
       Call the Python evaluate() function with the expression to be evaluated.
       The evaluate() function will call c_get_var_value() to obtain any
       variable values needed to evaluate the expression. It will use 
       caching and normal logical short-circuiting to reduce the number 
       of requests.
    */
    p_args = Py_BuildValue("(s)", expr);
    p_result = PyObject_CallObject(p_evaluate, p_args);
    Py_DECREF(p_args);
    if (PyErr_Occurred()) {
        PyErr_Print();
        return 1;
    }
    result = PyInt_AsLong(p_result);
    Py_DECREF(p_result);

    printf("result was %ld\n", result);

    Py_DECREF(p_module);
    Py_DECREF(p_evaluate);
    return 0;
}

Ответ 2

Я не знаю ни одной доступной библиотеки для этого.

Обычным подходом было бы построить дерево выражений и оценить, что возможно - подобно постоянной сгибанию в компиляторах: https://en.wikipedia.org/wiki/Constant_folding

Одним из важных аспектов этого является знать допустимые значения для переменных и, следовательно, разрешенные частичные оценки, например. x*00*x) является 0, если x является целым числом или конечным числом с плавающей запятой, но не может быть оценено, если x является плавающим числом IEEE (поскольку это может быть Nan или бесконечность) или если x может быть матрицей, так как [1,1]*0 есть [0,0] не скаляр 0.

Ответ 3

Один из способов - проанализировать выражение в дереве и оценить дерево. Подвыражения, для которых известны все переменные, будут полностью оценены. Эффект будет заключаться в том, чтобы упростить дерево.

В вашем примере дерево имеет && вверху с двумя поддеревьями, а слева - дерево для A>0. Чтобы оценить дерево, мы оцениваем левое поддерево, которое возвращает -1, и поэтому нам не нужно оценивать правильное поддерево, потому что оператор &&. Все дерево оценивается как false.

Ответ 4

Я не понимаю, что вы хотите делать или понимать, но я уверен в том, что ivan_pozdeev о оценке коротких замыканий и ленивой оценке.

Булево выражение оценивает слева направо, и когда результат известен, оценка останавливается и игнорирует то, что справа.

С Python:

E = "(A > 0) and (B > 5 or C > 10)"
A = -1
print(eval(E))

дает

False

Но

E = "(A > 0) and (B > 5 or C > 10)"
A = 1
print(eval(E))

дает ошибку "имя" B "не определено".

Ответ 5

Итак, из того, что я понимаю по вашему вопросу, вы хотите что-то вроде

if (A>0) {
  B = getB();
  C = getC();
  if (B>23 || C==11)
    explode();
}

т.е. ваше выражение должно быть разделено так, чтобы вы только работали с известными значениями.

Ответ 6

Вы можете сделать это следующим образом:

class LazyValues():

    def __init__(self):
        self._known_values = {}

    def __getitem__(self, var):
        try:
            return self._known_values[var]
        except KeyError:
            print("Evaluating %s..." % var)
            return self._known_values.setdefault(var, eval(var))


def lazy_eval(expr, lazy_vars):
    for var in lazy_vars:
        expr  = expr.replace(var, "lazy_values['%s']" % var)
        # will look like ((lazy_value['A']>0) && (lazy_value['B']>5 || lazy_value['C'] > 10))

    lazy_values = LazyValues()
    return eval(expr)


lazy_eval("((A>0) and (B>5 or C > 10))", lazy_vars=['A', 'B', 'C'])

# Evaluating A...
# ....
# NameError: name 'A' is not defined

A = -1
lazy_eval("((A>0) and (B>5 or C > 10))", lazy_vars=['A', 'B', 'C'])

#Evaluating A...
#False

A = 5
B = 6
lazy_eval("((A>0) and (B>5 or C > 10))", lazy_vars=['A', 'B', 'C'])

# Evaluating A...
# Evaluating B...
# True

Подробнее...

Ответ 7

Хотя это очень грубая реализация вашего решения, но она идеально подходит для вашей ситуации, хотя используется много if else и обработка исключений.

def main_func():
    def checker(a, b=None, c=None):
        if a is None:
            del a
        if b is None:
            del b
        if c is None:
            del c
        d = eval('a>0 and (b>5 or c>10)')
        return d
    return checker

def doer(a=None, b=None, c=None):
    try:
        return main_func()(a,b,c)
    except NameError as e:
        if "'a' is not" in str(e):
            return 'a'
        elif "'b' is not" in str(e):
            return 'b'
        elif "'c' is not" in str(e):
            return 'c'

def check_ret(ret):
    return type(ret) == bool

def actual_evaluator():
    getter = {
        "a": get_a,
        "b": get_b,
        "c": get_c
    }
    args = []
    while True:
        ret = doer(*tuple(args))
        if not check_ret(ret):
            val = getter[ret]()
            args.append(val)
        else:
            return ret

if __name__ == '__main__':
        print actual_evaluator()

Теперь, объясняя мой код, main_func возвращает другую функцию, которая используется для оценки данного выражения в строке. Хотя здесь строка была жестко запрограммирована, вы всегда можете передать ее как параметр функции и заменить строку внутри eval параметром.

В doer вызывается функция, возвращаемая main_func, и если вызывается a NameError, что происходит в случае, если предыдущие условия были ложными и вычислялись новые значения, тогда он возвращает определенную переменную, которая требуется для расчета. Все это проверяется в actual_evaluator, где значения переменных выбираются с помощью некоторой функции get_variable_name, которую вы можете определить в своем getter dict. В моем коде я использовал случайные числа для проверки действительности, но, как вы сказали, вам придется оценивать различные переменные другими способами, чтобы вы могли вызывать соответствующие функции.

Ответ 8

Мне кажется, что ответ да, да, вы можете попытаться оценить выражение с отсутствующей информацией. Вам нужно будет определить, что происходит, если сбой поиска символа.

В этом случае вам понадобится булевский анализатор выражений и таблица символов, чтобы оценщик мог искать символы для выполнения выражения.

Если вам удастся найти все символы, результат будет истинным или ложным. Если вам не удается найти символ, обработайте этот случай, возможно, верните None, nullptr или создайте/выбросите исключение.

Я считаю, что вы можете встроить интерпретатор python в вашу программу на С++ и вызвать функцию для оценки выражения, что более важно, вы можете дать ему dict для использования в качестве таблицы символов. Если вызов возвращает результат, он смог найти достаточное количество символов или ярлыков для результата, иначе он вызовет исключение, которое может обнаружить ваш код на С++.

Вы можете прототипировать функцию в python, чтобы оценить, работает ли этот подход так, как вы хотите, а затем вставлять.

Или вы можете сделать все это на С++, с грамматикой, лексером, парсером и лифтом.

Ответ 9

Из-за поведения короткого замыкания Python может оценивать выражение даже без всех содержащихся значений, если это возможно. Если нет, возникает исключение:

In [1]: a= False

In [3]: a and b
Out[3]: False

In [4]: a or b
NameError: name 'b' is not defined

Но выражение оценивается слева направо:

In [5]: b and a
NameError: name 'b' is not defined

Ответ 10

В прошлом я сделал подход "roll-my-own" для этого. Это не так сложно для простых вещей; вы просто создаете свои собственные объекты, которые реализуют магические математические методы и отслеживают другие объекты.

Если вам нужно что-то более полнофункциональное, проект sympy предназначен для выполнения символической математики...

Ответ 11

Я бы посмотрел на симплексные или другие системы компьютерной алгебры. Я считаю, что алгебраическое упрощение оценки pxeression plus short circuit позволит вам оценить все случаи, когда можно получить результат. Есть некоторые случаи, когда вам нужно знать значение некоторой переменной. Например, если у вас есть простое выражение типа == b, вы не достигнете прогресса, не зная значения a и b. Однако что-то вроде (a >= 0) || (a <= 0), алгебраическое упрощение приведет к истинному предположению, что a не является NAN или другим значением, которое не равно самому себе.

Ответ 12

Я исхожу из этого вопроса, что

  • у вас есть логическое выражение, зависящее от результата различных функций;
  • вы используете значения некоторых из этих функций более одного раза (возможно, прежде чем оценивать это выражение или, возможно, внутри этого выражения), поэтому вы хотите сохранить их результаты, чтобы не называть их дважды; и
  • вы хотите оценить логическое выражение и по пути, по которому вы хотите получить и сохранить значения для функций, которые ранее не выполнялись, но достаточно их для оценки выражения (используя обычное поведение короткого замыкания).

Я упомянул в другом ответе, что лучше всего использовать встроенное поведение короткого замыкания на С++. Чтобы сделать это и достичь цели 2, вам нужно будет использовать функции вместо переменных в логическом выражении. Таким образом, вы можете инициировать вычисление недостающего значения, когда оно нуждается в нем.

Ниже приведены два подхода к этому. Первый переносит ваши медленные функции с помощью универсальной кэширующей оболочки. Второй определяет пользовательский, кеширующий помощник для каждой из ваших медленных функций. После компиляции любой из них должен быть вызван с вашими значениями A, B и C для тестирования, например. evaluate_cached 10 9 -1. Они будут вести себя так, как вы хотите.

evaluate_cached.cpp

# include <stdio.h>
# include <stdlib.h>
# include <unordered_map>

static char **args;

// define (slow) functions to calculate each of the needed values
int A() {
  printf("Calculating value for A\n");
  return atoi(args[1]);
}

int B() {
  printf("Calculating value for B\n");
  return atoi(args[2]);
}

int C() {
  printf("Calculating value for C\n");
  return atoi(args[3]);
}

typedef int (*int_func)(void);

// wrapper to cache results of other functions
int cached(int_func func) {
    // Create an unordered_map to hold function results
    static std::unordered_map<int_func, int> results;

    if (results.find(func) == results.end()) {
        // function hasn't been called before; call and cache results
        results[func] = func();
    }
    return results[func];
}

int main(int argc, char *argv[])
{
    if (argc!=4) {
        fprintf(stderr, "%s must be called with 3 values for A, B and C.\n", argv[0]);
        return 1;
    } else {
        args = argv;
    }
    // do the evaluation, with short-circuiting
    if (((cached(A)>0) && (cached(B)>5 || cached(C) > 10))) {
        printf("condition was true\n");
    } else {
        printf("condition was false\n");
    }
    return 0;
}

evaluate_helpers.c

# include <stdio.h>
# include <stdlib.h>

static char **args;

// define (slow) functions to calculate each of the needed values
int calculate_A() {
  printf("Calculating value for A\n");
  return atoi(args[1]);
}

int calculate_B() {
  printf("Calculating value for B\n");
  return atoi(args[2]);
}

int calculate_C() {
  printf("Calculating value for C\n");
  return atoi(args[3]);
}

// define functions to retrieve values as needed,
// with caching to avoid double-calculation
int A() {
  static int val, set=0;
  if (!set) val=calculate_A();
  return val;
}
int B() {
  static int val, set=0;
  if (!set) val=calculate_B();
  return val;
}
int C() {
  static int val, set=0;
  if (!set) val=calculate_B();
  return val;
}

int main(int argc, char *argv[])
{
    if (argc!=4) {
        fprintf(stderr, "%s must be called with 3 values for A, B and C.\n", argv[0]);
        return 1;
    } else {
        args = argv;
    }
    // do the evaluation, with short-circuiting
    if (((A()>0) && (B()>5 || C() > 10))) {
        printf("condition was true\n");
    } else {
        printf("condition was false\n");
    }
    return 0;
}