Eval/Exec with assigning variable - Python

2020-05-05 01:02发布

问题:

The below piece of code is aimed at converting a factorial into its products. E.g. "4!" --> "(4*3*2*1)". This code does not work due to the line exec(codeToRun). However, if I instead put the value of codeToRun in place of exec(codeToRun) then it works perfectly so why doesn't exec work?

Doesn't work ↓

def checkSpecialChars(char, stringToCheck, codeToRun):
    while char in stringToCheck:
        currentString=""
        for i in range(len(stringToCheck)):
            if stringToCheck[i]==char:
                try:
                    eval(codeToRun)
                except:
                    exec(codeToRun)
                    print(stringToCheck)
                currentString=""
                break
            if stringToCheck[i].isdigit():
                currentString+=stringToCheck[i]
            else:
                currentString=""
    return stringToCheck

Does work ↓

def checkSpecialChars(char, stringToCheck, codeToRun):
    while char in stringToCheck:
        currentString=""
        for i in range(len(stringToCheck)):
            if stringToCheck[i]==char:
                try:
                    eval(codeToRun)
                except:
                    stringToCheck = stringToCheck[:i-len(currentString)] + "(" + "*".join(str(integer) for integer in range(int(currentString),0,-1)) + ")" + stringToCheck[i+1:]
                print(stringToCheck)
                currentString=""
                break
            if stringToCheck[i].isdigit():
                currentString+=stringToCheck[i]
            else:
                currentString=""
    return stringToCheck

EDIT #1 The number of factorials can be more than one and the number of digits in each factorial can be more than one as well.

Input: "11!/10!"

Expected Output: "(11*10*9*8*7*6*5*4*3*2*1)/(10*9*8*7*6*5*4*3*2*1)"

EDIT #2 I have added a print statement outputting the string as seen in the two pieces of code. Now when I run the program and enter 4!, the program pauses (as if it was an infinite loop). I then press CTRL+C to exit the program and it decides to output 4!. This then happens every time I press CTRL+C so the line must be running because the print statement occurs but it remains at 4!.

回答1:

Let's have a quick look at the docs:

Help on built-in function exec in module builtins:

exec(source, globals=None, locals=None, /) Execute the given source in the context of globals and locals.

The source may be a string representing one or more Python statements
or a code object as returned by compile().
The globals must be a dictionary and locals can be any mapping,
defaulting to the current globals and locals.
If only globals is given, locals defaults to it.

and

Help on built-in function locals in module builtins:

locals() Return a dictionary containing the current scope's local variables.

NOTE: Whether or not updates to this dictionary will affect name lookups in
the local scope and vice-versa is *implementation dependent* and not
covered by any backwards compatibility guarantees.

This final note would appear to explain your troubles.

To be more specific, you are calling exec with one parameter, so it will execute in the default environment of locals() on top of globals(). It is important to realize, that these are not necessarily identical with the actual global and local scopes, but may in theory be proxies or copies or whatever. Now, if you, for example, assign a variable one of these dictionaries gets updated accordingly. What the note in the docs for locals says, is that there is no guarantee that this update will be propagated to the actual local scope. And this I think is why your program doesn't work.

How to work around this:

Based on the above one easy fix is

(1) make a contract with yourself that codeToRun assigns to stringToCheck.

(2) keep a reference to the instance of locals() you pass to exec

(3) use this to explicitly set stringToCheck

So your except block would look somewhat like

    l = locals()
    exec(codeToRun, globals(), l)
    stringToCheck = l['stringToCheck']