Search code examples
python-3.xdynamicglobal-variablesundefinedname-lookup

In Python, when looking up an undefined global variable, is it possible to dynamically generate a value?


In Lua, global variables are stored in a table named _G. You can add a metatable to _G, such that when you lookup an undefined global value, a user defined function is called to provide a value.

In the below example, looking up any undefined variable returns the name of the undefined variable.

setmetatable ( _G, { __index = function ( t, k ) return k end } )

print ( foo )    --  prints the string "foo"
foo  =  5
print ( foo )    --  prints 5
print ( bar )    --  prints the string "bar"

Is it possible to achieve the same effect in Python 3?


Solution

  • Updated answer from Dec 12, 2018:

    Here is another (better?) way. Basically, we manually read(), compile(), and then exec() the entire source file. When we call exec(), we can pass in an alternative global dictionary. The entire file is read, compiled, and executed twice, but we do achieve the desired result.

    def  baz  ():
      global  foo
      print ( foo )    #  should print the string 'foo'                             
      foo  =  5
      print ( foo )    #  should print 5                                            
      print ( bar )    #  should print the string 'bar'                             
    
    
    class  CustomGlobals ( dict ):
      def  __getitem__  ( self, k ):
        if  k in self:  return  self .get ( k )
        if  hasattr ( self .get ( '__builtins__' ), k ):
          #  we raise KeyError to avoid clobbering builtins                         
          raise  KeyError
        return  k
    
    
    def  main  ():
    
      with  open ( __file__, 'rb' ) as f:
        source  =  f .read()    # re-read __file__                                  
      code  =  compile ( source, __file__, 'exec' )    #  re-compile __file__       
    
      g  =  CustomGlobals ( globals() )
      g [ '__name__' ]  =  None    #  prevent infinite recursion                    
      exec ( code, g )    #  re-exec __file__ against g                             
    
      g [ 'baz' ] ()    #  call the re-complied baz function                        
    
    if  __name__ == '__main__':  main()
    

    The above approach is superior (IMO) because we do not need to nest code in a string, and because error messages will contain correct line numbers.

    Original answer from Nov 29, 2018:

    If the Python code in question comes from a string (rather than a standard .py file), then you can exec() the string and provide a custom global dictionary as follows:

    code  =  '''                                                                     
    print ( foo )    #  should print the string 'foo'                               
    foo  =  5                                                                       
    print ( foo )    #  should print 5                                              
    print ( bar )    #  should print the string 'bar'                               
    '''
    
    class  CustomDict ( dict ):
      def  __init__  ( self, other ):  super() .__init__ ( other )
      def  __getitem__  ( self, k ):
        if  k in self:  return  self .get ( k )
        if  hasattr ( self .get ( '__builtins__' ), k ):
          #  we raise KeyError to avoid clobbering builtins 
          raise  KeyError
        return  k
    
    exec ( code, None, CustomDict ( globals() ) )
    

    As desired, the above outputs:

    foo
    5
    bar
    

    I have not found any way to "mutate" a module to achieve the same result for code that is in that module. If anyone knows of a way to mutate a module, I would be very happy to see it.

    (Perhaps a module could read its own source code into a string, then compile that string in the context of a custom global dict, and then inject the result into sys.modules? In other words, the module would replace itself with a clone of itself, but with a different global dict. Hmmm.)

    Aside: There are ways to simulate __getitem__() and __getattr__() on a module. And as of Python 3.7, you can simply and directly define a __getattr__() function in the module. However, as far as I can tell, none of these techniques can hook into the lookup of global variables. Sources: