4

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?

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • You want something like return reference in C++ ? – llllllllll Mar 02 '18 at 20:09
  • @liliscent Not sure what that is, but I added an example of exactly what I want. – Mad Physicist Mar 02 '18 at 20:11
  • If you only want to modify a variable in another module, just use `a.value` to access it. I don't know why you mention that function `x()`. – llllllllll Mar 02 '18 at 20:13
  • Because I want to make a copy of the function object in `b` that refers to `b`'s globals instead of `a`'s. – Mad Physicist Mar 02 '18 at 20:14
  • @liliscent. Perhaps it is more clear now? – Mad Physicist Mar 02 '18 at 20:17
  • I think you need a class instead of global variable. Essentially you want 2 identical function operating on different `value`. This `value` should be a class member. – llllllllll Mar 02 '18 at 20:18
  • @liliscent. I can do it for a class. However, I have a case where I can not avoid using globals as shown, but I would like to replace the source of them. An explanation of why that is not possible would answer my question as well. I suspect, however, that since this is Python, there is a way to do it. – Mad Physicist Mar 02 '18 at 20:19

3 Answers3

5

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.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
1

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.

user3483203
  • 50,081
  • 9
  • 65
  • 94
1

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

Mk Km
  • 94
  • 6
  • This assumes a much more specific interface than what I had in mind, but it certainly works in some cases. Also, this is better use of `eval` than usual. – Mad Physicist May 14 '20 at 15:03