Search code examples
pythonmagic-methodsgetattr

use __getattr__ within the same module or console


I have setup a custom __getattr__() method on a module which works perfectly when called from outside the module. However, it doesn't work from within the module itself (since that uses the globals() dict which doesn't redirect to __getattr__ on failure). How would I work around that?

I was thinking about encapsulating the globals() into a custom dict where I can then modify the __getitem__ method, but that seems very dirty and error prone. Is there another way to do that?

MWE:

def __getattr__(name):
    f'custom {name}'

print(a)  # should print 'custom a', but raises NameError

Solution

  • When a name is looked up in the global namespace, it is first looked up in the the globals() dict, and then the __builtins__ dict if that fails, so to customize the behavior of a missing name, you really want to customize the __builtins__ dict rather than the globals() dict.

    But the __builtins__ dict of the current running frame can't be reassigned, so one workaround would be to re-execute the current frame's code object after customizing __builtins__ with a dict with a __missing__ method defined to delegate access to the module's __getattr__ function.

    To avoid triggering the __builtins__ customization logics upon re-execution, you can check if __builtins__ is of the custom dict class before deciding whether to run the original intended code instead.

    This approach works both as a main program and as an imported module:

    if type(__builtins__).__name__ == '_DefaultDict':
        # your original code here
        def __getattr__(name):
            return f'custom {name}'
    
        print(a)
    else:
        import sys
        import builtins
    
        class _DefaultDict(dict):
            def __missing__(self, name):
                return __getattr__(name)
    
        __builtins__ = _DefaultDict(vars(builtins))
        del _DefaultDict # avoid namespace pollution
        exec(sys._getframe(0).f_code, globals())
    

    This outputs:

    custom a
    

    Demo: https://ideone.com/ThEpv6