Search code examples
pythonpython-3.xdynamicevalfunction-declaration

How to create a function with arbitrary parameters based on an arbitrary string


My end goal is: I want to create a set of truth tables, where each truth table corresponds to an arbitrarily defined boolean expression, which is originally stored as a string (like: "(var_1 and not var_2) or var_3" ). The string could have any number of operators.

This is easily achievable if I have a particular boolean expression in mind:

def evaluator(var_1,var_2,var_3):
    return  (var_1 and not var_2) or var_3

def truth_table(f):
    values = [list(x) + [f(*x)] for x in product([False,True], repeat=f.__code__.co_argcount)]
    return pd.DataFrame(values,columns=(list(f.__code__.co_varnames) + [f.__name__]))

one_truth_table = truth_table(evaluator)

But I want to do it for any function of any number of parameters, with any type of boolean expression. I will be iterating through boolean expressions as strings to create a series of truth tables.

I've been struggling with this all day. If I could get this snippet of code to behave as I want, then my problem would be solved.

def temp_func(boolean_expression_string,variable_names_list):
    return eval(boolean_expression_string)

# i have two strings: '(var_1 and var_2) and (var_3 or not var_4) or var_etc'
# and also: 'var_1,var_2,var_3,var_4,var_etc'

temp_func('(var_1 and var_2) and (var_3 or not var_4) or var_etc', input(list(eval('var_1,var_2,var_3,var_4,var_etc'))))

Running this the result is:

NameError: name 'var_1' is not defined

I included the whole back story in case I'm approaching the whole problem in a dumb way. Though you might guess that I'm just trying to get it to work, elegance isn't my top priority at the moment.

Edit: the variable names are not uniformly defined, and can't be parsed according to some ordering, so this is another layer of difficulty to deal with


Solution

  • eval can take two dictionaries of name to value mappings to use as the global and local namespaces, respectively, to run the expression in.

    New answer

    It's easier to ask forgiveness than it is to get permission.

    The idea is to find all variable names by trying to evaluate the expression and catching the NameError. Note that we need to generate all variable assignments for the list of variables because python will short circuit the evaluation of or and and. For example, in var_1 or var_2 we will not find var_2 if var_1 is initialised to True.

    def variable_names(expression):
        # list of found variables
        variables = list()
        while True:
            try:
                # generate all assignments for current variable names
                assignments = [
                    {variables[i]: v for i, v in enumerate(vs)}
                    for vs in itertools.product(
                        [True, False], repeat=len(variables)
                    )
                ]
                # try to evaluate them all
                for assignment in assignments:
                    eval(expression, None, assignment)
                # all of them work, can return
                return variables
            except NameError as e:
                # get next variable
                variables.append(
                    re.match("name '(.+)' is not defined", str(e)).group(1)
                )
    

    We then generate a list of dictionaries of assignments—exactly as in the previous method—and give this to eval, adding the result to the dictionary. The DataFrame can then be created from records.

    def truth_table(expression):
        # get variable names
        variables = variable_names(expression)
        # make list of assignments
        assignments = [
            {variables[i]: v for i, v in enumerate(vs)}
            for vs in itertools.product([True, False], repeat=len(variables))
        ]
        # get truthy values
        values = [
            {**assignment, **{"value": eval(expression, None, assignment)}}
            for assignment in assignments
        ]
        # make dataframe from records and supply column order
        return pd.DataFrame.from_records(values, columns=variables + ["value"])
    

    Old answer—fixed variable naming.

    If all of your variables are named var_1, var_2,... you can give a list of their value assignments to evaluator and parse them into a dictionary

    def evaluator(expression, values):
        return eval(
            expression,
            None,
            {"var_{}".format(i + 1): v for i, v in enumerate(values)},
        )
    

    and run it as follows

    evaluator( 
        "(var_1 and var_2) and (var_3 or not var_4)", 
        [True, False, True, False] 
    )                                                                                        
    

    which returns False.

    The full code for computing a truth table from an expression is then

    def truth_table(expression):
        # get variable names
        varnames = set(re.findall(r"(var_\d+)", expression))
        # sort by index
        varnames = sorted(varnames, key=lambda x: int(x.split("_")[1]))
        # get truthy values
        values = [
            list(x) + [evaluator(expression, x)]
            for x in itertools.product([True, False], repeat=len(varnames))
        ]
        return pd.DataFrame(values, columns=varnames + ["T/F"])
    

    If you have holes in the list of variables—e.g. (var_1 and var_3)—you'll need to either rename them before calling the evaluator or change the evaluator to take a dictionary.