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:
- 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.
-
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?
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.
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
.
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
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:
- Re-define
x
in moduleb
using copy-and paste. The real function is much more complex than shown, so this doesn’t sit right with me. -
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?
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.
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
.
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