Reputation: 346
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
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