Mailing List Archive

Strange namespace issue
Hi all,

today I faced an issue that, although very easy to fix, left me wondering
about what causes it.

The context is an application that execute small scripts coming from an
external source (say, a database). The application first compile the script,
then execute it passing it some values: in the script I wrote this morning I
used a list comprehension, executing a method of an instance passed in in the
local context.

The following example illustrates the problem:

class Test:
def create_value(self, a):
return a*2

script = """\
# This works
cv = test.create_value
x = []
for i in range(3):
x.append(cv(i))
print(x)

# This does not: NameError: name 'test' is not defined
x = [test.create_value(i) for i in range(3)]
print(x)

# This neither: NameError: name 'cv' is not defined
x = [cv(i) for i in range(3)]
print(x)
"""

code = compile(script, 'script', 'exec')
exec(code, {}, {'test': Test()})

I must be missing something, as I cannot understand why within the list
comprehension the neither the name "test" nor the name "cv" are defined...

Thanks in advance for any enlightenment!

ciao, lele.
--
nickname: Lele Gaifax | Quando vivrò di quello che ho pensato ieri
real: Emanuele Gaifas | comincerò ad aver paura di chi mi copia.
lele@metapensiero.it | -- Fortunato Depero, 1929.

--
https://mail.python.org/mailman/listinfo/python-list
Re: Strange namespace issue [ In reply to ]
On Tue, Aug 11, 2020 at 5:44 AM Lele Gaifax <lele@metapensiero.it> wrote:
>
> Hi all,
>
> today I faced an issue that, although very easy to fix, left me wondering
> about what causes it.
>
> The context is an application that execute small scripts coming from an
> external source (say, a database). The application first compile the script,
> then execute it passing it some values: in the script I wrote this morning I
> used a list comprehension, executing a method of an instance passed in in the
> local context.
>
> The following example illustrates the problem:
>
> class Test:
> def create_value(self, a):
> return a*2
>
> script = """\
> # This works
> cv = test.create_value
> x = []
> for i in range(3):
> x.append(cv(i))
> print(x)
>
> # This does not: NameError: name 'test' is not defined
> x = [test.create_value(i) for i in range(3)]
> print(x)
>
> # This neither: NameError: name 'cv' is not defined
> x = [cv(i) for i in range(3)]
> print(x)
> """
>
> code = compile(script, 'script', 'exec')
> exec(code, {}, {'test': Test()})
>
> I must be missing something, as I cannot understand why within the list
> comprehension the neither the name "test" nor the name "cv" are defined...
>
> Thanks in advance for any enlightenment!
>

Interesting. You're passing an empty globals and a non-empty locals
(the second and third arguments to exec, respectively). Is that
deliberate? By the look of this code, it's meant to be at global scope
(as if it's the top level code in a module), which is best done by
passing just a single dict to exec (it'll be both globals and locals).

The reason your first block works but the comprehensions don't is that
comprehensions act in a nested scope. I think you've hit on a curious
edge case where empty globals results in strange behaviour.

I tried your code with the empty dict removed and it appears to work,
but if you had a good reason for having separate dicts, that might
break.

ChrisA
--
https://mail.python.org/mailman/listinfo/python-list
Re: Strange namespace issue [ In reply to ]
Chris Angelico <rosuav@gmail.com> writes:

> Interesting. You're passing an empty globals and a non-empty locals
> (the second and third arguments to exec, respectively). Is that
> deliberate? By the look of this code, it's meant to be at global scope
> (as if it's the top level code in a module), which is best done by
> passing just a single dict to exec (it'll be both globals and locals).
>
> The reason your first block works but the comprehensions don't is that
> comprehensions act in a nested scope. I think you've hit on a curious
> edge case where empty globals results in strange behaviour.
>
> I tried your code with the empty dict removed and it appears to work,
> but if you had a good reason for having separate dicts, that might
> break.

Thank you Chris,

yes, you are right and indeed passing the external context in the "globals"
argument *AND* omitting the "locals" one I get the expected outcome, and yes,
I'm surprised, the snake still bites sometime, after several dozens of years :)

However, I'm still unclear on *when* it is safe & useful to pass the third
argument to exec(): while in the particular case I was working it does not
make any difference (passing the context in the second "globals" argument,
that is), in at least one other case I am relying on the fact that passing the
third argument the script "stores" there whatever it defines, and the caller
examines that to make some further decision. In other words, something like
this:

script = """\
def function_a():
return ['a' * repetitions]

def function_b():
return [['a'] * repetitions]

precomputed = repetitions ** 10

def function_c():
return precomputed
"""

code = compile(script, 'script', 'exec')
globs = {'repetitions': 3}
locs = {}
exec(code, globs, locs)

for n in locs:
print(f"Oh, you defined {n!r}...")
v = locs[n]
if callable(v):
print(" a callable, returning", v())
else:
print(" a value,", v)

In this case, it would be less practical to determine what the script defined:
by any chance this case is the first I wrote, and here the choice to pass the
third argument was surely "deliberate".

But I see this is somewhat fragile, and wonder about a proper fix, but isn't
that a reasonable usage of the "locals" argument to exec()?

Ciao, lele.
--
nickname: Lele Gaifax | Quando vivrò di quello che ho pensato ieri
real: Emanuele Gaifas | comincerò ad aver paura di chi mi copia.
lele@metapensiero.it | -- Fortunato Depero, 1929.

--
https://mail.python.org/mailman/listinfo/python-list
Re: Strange namespace issue [ In reply to ]
On Wed, Aug 12, 2020 at 12:03 PM Lele Gaifax <lele@metapensiero.it> wrote:
> But I see this is somewhat fragile, and wonder about a proper fix, but isn't
> that a reasonable usage of the "locals" argument to exec()?
>

I'm not sure. Passing a locals argument to eval() I have sometimes
done, but never exec(). I've always exec'd code at top level and then
extracted something from globals - it seems the easiest.

ChrisA
--
https://mail.python.org/mailman/listinfo/python-list