-
Notifications
You must be signed in to change notification settings - Fork 6.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix NameError
when a pasted function attempts to access a variable in its outer scope
#2180
Conversation
NameError
when a pasted function attempts to access a variable in its outer scope
Tested on manim, looks like it works! (: master branch: (omitting logs and other verbose stuff) $ manimgl example_scenes.py OpeningManimExample -e
ManimGL v1.6.1
Exception reporting mode: Verbose
Python 3.12.5 (main, Aug 11 2024, 21:06:20) [GCC 13.2.1 20230801]
IPython 8.28.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: globals() is locals()
Out[1]: False
In [2]: new_text = TexText("Reproducing the issue on the master branch...")
In [3]: def make_blue():
...: new_text.set_color(BLUE)
...:
In [4]: make_blue()
NameError: name 'new_text' is not defined In my branch: $ manimgl example_scenes.py OpeningManimExample -e
ManimGL v1.6.1
Exception reporting mode: Verbose
Python 3.12.5 (main, Aug 11 2024, 21:06:20) [GCC 13.2.1 20230801]
IPython 8.28.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: globals() is locals()
Out[1]: True
In [2]: new_text = TexText("Testing the fix...")
In [3]: def make_blue():
...: new_text.set_color(BLUE)
...:
In [4]: make_blue()
In [5]: self.play(Write(new_text))
Out[5]: <example_scenes_insert_embed.OpeningManimExample at 0x75dc040e7a70> In the last line, the |
Thank you, that’s a helpful explanation. For the use case of embedded animation, having globals match locals seems fine, but in making it more systematic, I’m inclined to ask what kind of unintended consequences could arise from those dictionaries always matching. if this is the state of affairs in vanilla ipython, the answer may well simply be “none”. |
It works well for me, thanks! |
I didn't understand exactly in what other environment this would happen, besides embedded animation. The fix only affects Scene.embed, so I thought it would only affect the use case of dropping the user into a shell for interacting with the animation. (Which by the way, I didn't say that because I wanted to focus on the scope issue, but this little interactive shell integrated with the animation is absolutely amazing, I'm completely in love with playing with that haha)
Yeah, this is normal Python execution, I wouldn't worry about that. What I think that could be something to worry about is the fact that we're flattening two namespaces in one, and this could lose information. E.g., say you had a nested function that for some reason iterates through These are the kind of things that I think could go wrong, but (and now this is totally personal) I think that having the nested functions completely disconnected from the "locals()" namespace is worse. Anyway, I'm glad that it worked for you! |
You're right that's the only environment this is meant for, I was just throwing out the question of what unintended consequences there could be within that environment. The potentials you list out associated with making both namespaces into one feel like they would only arise in pathological situations, so I'm not too worried there. As you mentioned, it's way better than the previous state of affairs. I'm glad you're enjoying playing with it! |
Motivation
The motivation is the variable scope problem mentioned in the How I animate 3Blue1Brown | A Manim demo with Ben Sparks video.
This is what's going on:
When you run
exec
(oreval
), those two last arguments are a globals namespace and a locals namespace. Thex = 9
line modifies the locals namespace, but theprint(x)
line inside the function reads from the globals namespace. We need them to be the same.When IPython runs our code, it calls exec/eval like this:
In a vanilla ipython shell, these two attributes are the same dict instance:
Our problem is that in an IPython shell acquired via
InteractiveShellEmbed.instance()
, these two namespaces are distinct, so the "function can't access outer scope variables" issue will always happen. (And no,nonlocal
doesn't help --nonlocal
is only for nested functions.)Directly instantiating a new shell is, I believe, the best way to avoid this problem. We can see here that the
user_module
anduser_ns
arguments are forwarded toInteractiveShellEmbed.init_create_namespaces
, which will set the attributesshell.user_module
(from whichshell.user_global_ns
takes the global namespace) andshell.user_ns
, and also setshell.default_user_namespaces
toFalse
. Callinginit_create_namespaces
directly on an already-instantiated shell doesn't seem to be a good idea because of this.Proposed changes
The order was:
InteractiveShellEmbed.instance()
.local_ns
dict as a copy ofcaller_frame.f_locals
.local_ns
.get_module(caller_frame.f_globals["__file__"])
, which will be the shell's global namespace.shell
passinglocal_ns
and the module object.This is the new order:
InteractiveShellEmbed(user_module=module)
. This guaranteesshell.user_global_ns is shell.user_ns
.local_ns
as a copy of the caller locals.local_ns
with the shortcuts.local_ns
. This is not a problem, because this module object was instantiated exactly for being used as the shell's global ns, and, inside the shell scope,local_ns
would shadow the module globals anyway. Also, module objects are never copied, so updating this instance here (after having already instantiated the shell using the module) does update the namespace that will be seen by the shell.shell()
without any parameters:local_ns
: is already captured bt the updated module globals.stack_depth
: is not used, becauseshell.default_user_namespaces
isFalse
.module
: was already mentioned when we instantiated the shell.Test
I tested my MWE, but I didn't test this fix in manim itself. I will remove the draft status from the PR after I have made sure that this fix actually helps manim.