Search code examples
pythonglobal-variables

Override globals in function imported from another module


Let's say I have two modules:

a.py

value = 3
def x()
    return value

b.py

from a import x
value = 4

My goal is to use the functionality of a.x in b, but change the value returned by the function. Specifically, value will be looked up with a as the source of global names even when I run b.x(). I am basically trying to create a copy of the function object in b.x that is identical to a.x but uses b to get its globals. Is there a reasonably straightforward way to do that?

Here is an example:

import a, b

print(a.x(), b.x())

The result is currently 3 3, but I want it to be 3 4.

I have come up with two convoluted methods that work, but I am not happy with either one:

  1. Re-define x in module b using copy-and paste. The real function is much more complex than shown, so this doesn't sit right with me.
  2. Define a parameter that can be passed in to x and just use the module's value:

    def x(value):
        return value
    

    This adds a burden on the user that I want to avoid, and does not really solve the problem.

Is there a way to modify where the function gets its globals somehow?


Solution

  • I've come up with a solution through a mixture of guess-and-check and research. You can do pretty much exactly what I proposed in the question: copy a function object and replace its __globals__ attribute.

    I am using Python 3, so here is a modified version of the answer to the question linked above, with an added option to override the globals:

    import copy
    import types
    import functools
    
    def copy_func(f, globals=None, module=None):
        """Based on https://stackoverflow.com/a/13503277/2988730 (@unutbu)"""
        if globals is None:
            globals = f.__globals__
        g = types.FunctionType(f.__code__, globals, name=f.__name__,
                               argdefs=f.__defaults__, closure=f.__closure__)
        g = functools.update_wrapper(g, f)
        if module is not None:
            g.__module__ = module
        g.__kwdefaults__ = copy.copy(f.__kwdefaults__)
        return g
    

    b.py

    from a import x
    value = 4
    x = copy_func(x, globals(), __name__)
    

    The __globals__ attribute is read-only, which is why it must be passed to the constructor of FunctionType. The __globals__ reference of an existing function object can not be changed.

    Postscript

    I've used this enough times now that it's implemented in a utility library I wrote and maintain called haggis. See haggis.objects.copy_func.