2

I am coding a kind of command line user interface where an arbitrary boolean expression can be given as input. This expression must be repeatedly evaluated on a dictionary that is changing after some update.

Here is the simplified code :

import traceback


def update_my_dict():
    return {'a': 1}


my_dict = {'a': 0}

bool_exp = input()

# -- verify code can be executed in the current context
try:
    result = eval(bool_exp, my_dict)
except Exception:
    print(f'Expression cannot be evaluated, evaluation raise the following error:')
    print(traceback.format_exc())
    quit()

# -- verify code return a boolean
if not isinstance(result, bool):
    print(f'Expression return {type(result)}, expect bool')
    quit()

# -- go
while not eval(bool_exp, my_dict):
    my_dict = update_my_dict()

Before running the last while loop I want to verify that the expression can be executed in the current context and ensuring that its return a boolean.

My problem is if the expression is, for example bool_exp = a == 1 and b == 2 the first test evaluation of the expression while return false but do not raise exception because off lazy evaluation. But when my_dict is updated then an error will be raised.

So is that possible to, some how, disable the lazy/short-circuit evaluation for the first test evaluation ? I searched some solution using ast, but it seems complicated since bool_exp can be arbitrary long and complex, like containing entangled boolean expressions and so on.

PS: I know eval() is unsafe in a general context but my code will not be usable externaly

PS2: I know its possible to catch the exception in the while loop but it looks a bit sub-optimized knowing my_dict keys will never change when updated, only their values. Also this question is more like if its possible to control the evaluation behavior

EDIT

"but you could use & and | instead of and and or !" No. I cannot tell what will be entered as input. So the user can input whatever s.he want.

A Correct input expression:

  1. Should return a boolean.
  2. Should only involve dictionary keys in the tests.

"Correct" meaning it will be repeatedly evaluated in the ending while loop without raised any exception. We assume that the keys in the dictionary will stay constant. i.e only the values of the dict will change during the update phase. In other words, the first try/except aim to verify that the expression is only doing some test on variables that are in my_dict.

I cannot explain further the global use of this or I'll need a very long post with lost of what seems irrelevant information to resolve the issue.

Welgriv
  • 714
  • 9
  • 24
  • The unwanted behaviour is better described as 'short-circuit boolean evaluation'. Lazy evaluation is a different thing. Maybe editing your question along these lines will encourage answers? – L.Grozinger Aug 05 '21 at 14:48
  • 3
    Does this answer your question? [Python: Avoid short circuit evaluation](https://stackoverflow.com/questions/12281469/python-avoid-short-circuit-evaluation) – L.Grozinger Aug 05 '21 at 14:49
  • Can you clarify what you are trying to achieve and how it depends on "lazy evaluation"? From what I am gathering, your issue is that ``a == 1 and b == 2`` allows ``b`` to be undefined when ``a`` equals ``1`` – and you consider this "invalid". But what exactly is your *criteria* for "invalid"? Would ``a == "12"`` be invalid? Would ``a == 12 and []`` be invalid? What about ``a == a and b``? How is ``my_dict`` updated anyway? – MisterMiyagi Aug 05 '21 at 14:49
  • What is "the first test evaluation of the expression"? Is that your first iteration of the while loop or your first `eval` in your first `try/catch` block? (Just trying to fully digest the question). Also, what is happening in your dictionary that it uses the evaluation of this string expression and errors out? – JNevill Aug 05 '21 at 14:49
  • 2
    May I ask what is it that you really want to achieve with this code? – Ayush Gupta Aug 05 '21 at 14:51
  • @L.Grozinger have you read my question and the other one ? My input are arbitrary so I cannot modify the code that is evaluated. Not like this other question. – Welgriv Aug 05 '21 at 14:54
  • 1
    @Welgriv but the problem is the boolean operators right? which you could easily `replace` with non-short-circtuiing counterparts `&` and `|` as is suggested in the answer to the linked question? – L.Grozinger Aug 05 '21 at 14:55
  • @L.Grozinger I cannot tell if the input will be composed with `and` `or` or `&` an `|` because its an.... input ! you got it ! :) – Welgriv Aug 05 '21 at 14:58
  • 1
    @L.Grozinger The binary operators ``&`` and ``|`` have different precedence than the logical operators ``and`` and ``or``. Most importantly, they are on opposite sides of the comparison operators ``==``, ``<``, …. – MisterMiyagi Aug 05 '21 at 14:58
  • @MisterMiyagi very good point! Maybe there is a solution for that also... – L.Grozinger Aug 05 '21 at 15:00
  • I think the proper solution is not to repurpose the Python language/evaluation for something that is desired to have different semantics. :/ – MisterMiyagi Aug 05 '21 at 15:01
  • Why not have all that "verification" code as part of the loop? – Tomerikoo Aug 05 '21 at 15:06
  • If you want to stick to using Python to evaluate this, you can inspect the AST of the source code to check for free variables and reject any unwanted operations. Trying to change evaluation semantics will be neither feasible nor sufficient for your goal. – MisterMiyagi Aug 05 '21 at 15:13
  • @Tomerikoo please read the post until the end. Its because it certainly be slower. And its also dumb because expression will be evaluate twice. – Welgriv Aug 05 '21 at 15:13
  • 1
    No it will not. You are already evaluating it with each update: `while not eval(bool_exp, my_dict):`. The only added operation is `if not isinstance(result, bool):` which I ***highly*** doubt will affect the performance in any way – Tomerikoo Aug 05 '21 at 15:15
  • @Tomerikoo I really don't get how adding some code in the loop will not slow it down even a little ?? Beside its *possible* to verify the correctness of the expression before so it does not seems to be a satisfying workaround. – Welgriv Aug 05 '21 at 15:20
  • I might not have read all the comments here, but 1) If you are planning on using the builtin `eval(..)`, you no option around lazy evaluation, 2) What I'd do is do an `ast.literal_eval(..)`, and walk the tree and evaluate it myself. – UltraInstinct Aug 05 '21 at 15:20
  • @Tomerikoo cannot open your link without an account – Welgriv Aug 05 '21 at 15:22
  • @UltraInstinct yes `eval()` does optimize around lazy evaluation. Just test it and you'll see. – Welgriv Aug 05 '21 at 15:22
  • 1
    I've hammered open the question since the duplicate does not apply. The expression is provided as arbitrary input; manually inputting a "better" input is not possible in this situation. – MisterMiyagi Aug 05 '21 at 15:23
  • @Welgriv: Yes, I know :). What I meant to say was you will have to write your own evaluator the hard way -- if the input is going to be arbitrary. I don't see any way around that to achieve what you're hoping for. Any and all hacks being suggested here will have likely gotchas. – UltraInstinct Aug 05 '21 at 15:31
  • @Welgriv To do this properly it will need "some solution using ast" and that frankly takes some work to write down correctly. If someone makes the effort, it will take some time. – MisterMiyagi Aug 05 '21 at 15:31

1 Answers1

1

You could put the "verification" code inside the loop. This makes sense as your input is changing so you should verify it on each change. You already evaluate the expression every time the dict values change, so the only added logic compared to your current code is that the isinstance check is now done as well with every change of the dict:

import traceback

def update_my_dict():
    return {'a': 1}

my_dict = {'a': 0}

bool_exp = input()

# -- go
while True:
    # -- verify code can be executed in the current context
    try:
        result = eval(bool_exp, my_dict)
    except Exception:
        print(f'Expression cannot be evaluated, evaluation raise the following error:')
        print(traceback.format_exc())
        quit()

    # -- verify code return a boolean
    if not isinstance(result, bool):
        print(f'Expression return {type(result)}, expect bool')
        quit()

    if result:
        break

    my_dict = update_my_dict()

On the same example input a == 1 and b == 2 this will output:

Expression cannot be evaluated, evaluation raise the following error:
Traceback (most recent call last):
  File "main.py", line 14, in <module>
    result = eval(bool_exp, my_dict)
  File "<string>", line 1, in <module>
NameError: name 'b' is not defined
Tomerikoo
  • 18,379
  • 16
  • 47
  • 61