rakesh
rakesh

Reputation: 346

evaluate logical expression from string inputs

i am trying to rewrite a conditional operation purely using functions and introduce operators and arguments as variables.

for example something like 4>5 will be replaced with

import operator
arg1 = 4
arg2 = 5
result = operator.gt(arg1,arg2)

i would want to replicate the same for a complex expression that use multiple operators

for example in below case data is the input numbers will need to replaced with a function that can have values and operator supplied

data = {'val1':1,'val2':2,'val3':3,'val4':4,'val5':5,'val6':6}
exression_output = (data['val1'] > 0.5) & (data['val2'] == 2) & (data['val3'] >= 2)

what i did first is manually replace them like below
import operator
ops = {
    '+' : operator.add, '-' : operator.sub, '*' : operator.mul, '/' : operator.truediv,  # use operator.div for Python 2
    '%' : operator.mod, '^' : operator.xor, '<' : operator.lt, '<=' : operator.le,
    '==' : operator.eq, '!=' : operator.ne, '>' : operator.gt, '>=' : operator.ge,
    '&' :operator.and_
}
def eval_logical_operator(data,operations):
    if (len(operations)==1):
        return ops[operations[0]['operation']](data[operations[0]['op1']], operations[0]['op2'])
    if (len(operations)==2):
        return operator.and_(
                ops[operations[0]['operation']](data[operations[0]['op1']], operations[0]['op2']),
                ops[operations[1]['operation']](data[operations[1]['op1']], operations[1]['op2'])
                )
    if (len(operations)==3):
        return operator.and_(
                    operator.and_(
                        ops[operations[0]['operation']](data[operations[0]['op1']], operations[0]['op2']),
                        ops[operations[1]['operation']](data[operations[1]['op1']], operations[1]['op2'])
                    ),
                    ops[operations[2]['operation']](data[operations[2]['op1']], operations[2]['op2'])
        )
    #return new_df[operator.gt(op1, op2)]
    
eval_logical_operator(data,[{'op1':'val1','operation':'>','op2':0.5},])
eval_logical_operator(data,[
                            {'op1':'val1','operation':'>','op2':0.5},
                            {'op1':'val2','operation':'==','op2':2},
                            ]
                        )

eval_logical_operator(data,[
                            {'op1':'val1','operation':'>','op2':0.5},
                            {'op1':'val2','operation':'==','op2':2},
                            {'op1':'val3','operation':'>=','op2':2},
                            ]
                        )

to remove limitation of numbers of condition(if blocks) i replaced it with below expression. it works, but can work only for And operations

def eval_expression(data, operations):
    output = []
    for exp in operations:
        expression_output = ops[exp['operation']](data[exp['op1']],exp['op2'])
        output.append(expression_output)
    
    return all(output)
        
eval_expression(data,[
                            {'op1':'val1','operation':'>','op2':0.5},
                            {'op1':'val2','operation':'==','op2':2},
                            {'op1':'val3','operation':'>=','op2':2},
                      ]
               )

this would fail if an 'or' is introduced like below (data['val1'] > 0.5) & (data['val2'] == 2) | (data['val3'] >= 2)

can someone suggest a workaround this so that i can do this for a large complex expression like

data = {'val1':1,'val2':2,'val3':3,'val4':4,'val5':5,'val6':6}
output = (((data['val1'] > 0.5) & (data['val2'] == 2)) | (data['val3'] >= 2))&(data['val4'] >= 3)

i am willing to rewrite my input expression to accommodate this.

eval_logical_operator(data,[
                            {'op1':'val1','operation':'>','op2':0.5},
                            {'op1':'val2','operation':'==','op2':2},
                            {'op1':'val3','operation':'>=','op2':2},
                            ]
                        )

Upvotes: 0

Views: 174

Answers (1)

Achxy_
Achxy_

Reputation: 1201

This is how this problem could be approached with Abstract Syntax Tree (AST)

Firstly we will parse the python code into a tree, one standard implementation include ast in the python standard library (see: https://docs.python.org/3/library/ast.html)

Then we convert every BinOp Node to a Call node mapping from operations like +, - to operator.add and operator.sub for example. This can be done by subclassing ast.NodeTrasnformer then implementing visit_BinOp to return appropriate Call object.

This is an example of code a source to source compilation:

from ast import (
    BinOp,
    Call,
    ImportFrom,
    Load,
    Module,
    Name,
    NodeTransformer,
    alias,
    fix_missing_locations,
    parse,
    unparse,
)
from collections.abc import Iterable
from typing import Any

bin_op_special_cases = {
    "Mult": "mul",
    "Div": "truediv",
    "BitOr": "or_",
    "BitXor": "xor",
    "BitAnd": "and_",
    "MatMult": "matmul",
}


def _get_bin_conversion(op_name: str) -> str:
    return bin_op_special_cases.get(op_name, op_name.lower())


def _get_cls_name_of(obj: Any) -> str:
    cls = type(obj)
    return cls.__name__


class OperationNodeTransformer(NodeTransformer):
    def __init__(self, tree) -> None:
        self.operator_import_symbols = set()
        self.result = fix_missing_locations(self.visit(tree))

    def visit_BinOp(self, node: BinOp) -> Call:
        lhs, rhs = node.left, node.right
        op_name = _get_bin_conversion(_get_cls_name_of(node.op))
        self.operator_import_symbols.add(op_name)
        new_node = OperationNodeTransformer(
            Call(
                func=Name(id=op_name, ctx=Load()),
                args=[lhs, rhs],
                keywords=[],
            )
        )
        self.operator_import_symbols.update(new_node.operator_import_symbols)
        return new_node.result


def _is_ImportFromFuture(node) -> bool:
    return isinstance(node, ImportFrom) and node.module == "__future__"


def _add_ImportFromNode(
    mod: Module, mod_name: str, symbols: Iterable[str], level: int = 0
) -> None:
    node = ImportFrom(
        module=mod_name,
        names=[alias(name=name) for name in symbols],
        level=level,
    )
    body = mod.body
    first_expr = body[0]
    body.insert(int(_is_ImportFromFuture(first_expr)), node)


def convert_operations(py_code: str) -> str:
    tree: Module = parse(py_code)
    new = OperationNodeTransformer(tree)
    _add_ImportFromNode(new.result, "operator", new.operator_import_symbols)
    return unparse(new.result)


sample = """\
def foo(a, b):
    return a + b
"""

print(convert_operations(sample))

# Result :
# from operator import add

# def foo(a, b):
#     return add(a, b)

Now we can successfully convert python into new python code, although the above code has only been implemented for the following types :

class ast.Add
class ast.Sub
class ast.Mult
class ast.Div
class ast.FloorDiv
class ast.Mod
class ast.Pow
class ast.LShift
class ast.RShift
class ast.BitOr
class ast.BitXor
class ast.BitAnd
class ast.MatMult

(does not fully cover the operator alternatives, but the code is already way too big to be solitarily included in a single answer)

Example of a Fibonacci function being converted using convert_operation :
Old :

def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)


print(fib(5))

Converted :

from operator import sub, add

def fib(n):
    if n <= 1:
        return n
    return add(fib(sub(n, 1)), fib(sub(n, 2)))
print(fib(5))

(For <= to be converted, you'll have to implemented ast.Compare with visit_Compare which was too much to be included in this answer)

Hope that helps...

Upvotes: 1

Related Questions