pythonglobal-namespace

Global namespace changes depending on scope?


I have the following function defined in my Spyder startup file:

# startup.py
#-----------
def del_globals(*names):
   for name1 in names:
      try: del globals()[name1]
      except KeyError: pass # Ignore if name1 doesn't exist

At the Spyder console, I run the following script with the debugger to create a variable cat before descending into a function:

# MyScript.py
#------------
runfile('The/Path/to/startup.py') # In case functions are deleted
cat='dog'
del_globals('cat')

I have a breakpoint at the last line del_globals('cat'). Upon breaking, I confirm that cat is a variable in the global namespace:

sorted(globals().keys())
# ['TKraiseCFG', '__builtins__', '__doc__', '__file__',
#  '__loader__', '__name__', '__nonzero__', '__package__',
#  '__spec__', 'cat', 'del_globals']

I then step into del_globals('cat') and at the very first line (the for statement), confirmed that cat is not in the global namespace

sorted(globals().keys())
['TKraiseCFG', '__builtins__', '__doc__', '__loader__',
 '__name__', '__nonzero__', '__package__', '__spec__',
 '_spyderpdb_builtins_locals', '_spyderpdb_code',
 '_spyderpdb_locals', 'del_globals']

Should there be just one global namespace, and should that be the prevailing namespace at the REPL command line?

P.S. For background, the context is described here and here.


Solution

  • globals in Python are really per module.

    So, the call to globals() inside startup.py file will return you the global variables defined in that file (which will include the del_globals function itself.

    What Spyder is doing is injecting all function definitions from startup.py in the global namespace of the module used interactiely at the console. This injection is equivalent to what one gets in statements like from startup import del_globals: a reference to the function in the other module is bound to the current module globals with the same name. But the function object itself is bound to the global variables from that module - it is easy to introspect this, they are bound to the __globals__ attribute of the function itself:

    import startup
    from startup import del_globals
    
    del_globals.__globals__ is startup.__dict__
    # should return True
    

    The fix for this is possible, but requires advancing on Python introspection capabilities: a function can "know" the running environment from where it was called by introspecting Frame objects - these are runtime objects that bind together various stateful attributes needed to run any python code - and form a stack, one frame pointing to the one that preceded it in its .f_back attribute - and they also contain a reference to their own "globals" namespace.

    All that said, this should work as you intend:

    import inspect
    
    def del_globals(*names):
       caller_frame = inspect.currentframe().f_back
       caller_globals = caller_frame.f_globals
       for name1 in names:
          try: del caller_globals[name1]
          except KeyError: pass # Ignore if name1 doesn't exist