Override globals in function imported from another module

Question:

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?

Asked By: Mad Physicist

||

Answers:

So I found a way to (sort of) do this, although I don’t think it entirely solves your problems. Using inspect, you can access the global variables of the file calling your function. So if you set up your files like so:

a.py

import inspect

value = 3

def a():
    return inspect.stack()[1][0].f_globals['value']

b.py

from a import a

value = 5

print(a())

The output is 5, instead of 3. However, if you imported both of these into a third file, it would look for the globals of the third file. Just wanted to share this snippet however.

Answered By: user3483203

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.

Answered By: Mad Physicist

I had the same problem. But then I remembered eval was a thing.
Here’s a much shorter version(if you don’t need arguments):
b.py:

from a import x as xx

# Define globals for the function here
glob = {'value': 4}
def x():
    return eval(xx.__code__, glob)

Hopefully after 2 years it’ll still be helpful

Answered By: Mk Km
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.