I'm exploring what's possible to do in Python and recently came across this question: after running a function, is it possible to programmatically determine whether it has referenced anything out-of-scope? For example:
import module1
y = 1
def foo1(x):
return y + x # yes - it has referenced 'y' which is out of foo1 scope
def foo2(x):
return module1.function1(x) # yes - it has referenced 'module1' which is out of foo2 scope
def foo3(x):
return x*x # no - it has only referenced 'x' which is an input to foo3, so the code executed within the scope of foo3
Are there some reflection / analysis tools for this? Maybe this could be achieved with the 'trace' module somehow?
You can use inspect.getclosurevars:
Get the mapping of external name references in a Python function or method func to their current values. A named tuple ClosureVars(nonlocals, globals, builtins, unbound) is returned. nonlocals maps referenced names to lexical closure variables, globals to the function’s module globals and builtins to the builtins visible from the function body. unbound is the set of names referenced in the function that could not be resolved at all given the current module globals and builtins.
Let's see what it returns for your examples:
Case 1:
def foo1(x):
return y + x # yes - it has referenced 'y' which is out of foo1 scope
inspect.getclosurevars(foo1)
# ClosureVars(nonlocals={}, globals={'y': 1}, builtins={}, unbound=set())
Here it detected you are using variable in the global scope.
Case 2:
import colorsys
def foo2(x):
return colorsys.rgb_to_hsv(*x) # yes - it has referenced 'colorsys' which is out of foo2 scope
inspect.getclosurevars(foo2)
# ClosureVars(nonlocals={}, globals={'colorsys': <module 'colorsys' from '...\\lib\\colorsys.py'>}, builtins={}, unbound={'rgb_to_hsv'})
Here it detected you are using a module's function.
Case 3:
def foo3(x):
return x*x # no - it has only referenced 'x' which is an input to foo3, so the code executed within the scope of foo3
inspect.getclosurevars(foo3)
# ClosureVars(nonlocals={}, globals={}, builtins={}, unbound=set())
Here we see "nothing unusual".
Therefore, we can wrap this procedure in a function which looks if any of the fields (except for built-ins; i.e. foo can use abs and no problem) is non-empty:
import inspect
def references_sth_out_of_scope(fun):
closure_vars = inspect.getclosurevars(fun)
referenced = any(getattr(closure_vars, field) for field in closure_vars._fields if field != "builtins")
return referenced
Usage:
>>> references_sth_out_of_scope(foo1)
True
>>> references_sth_out_of_scope(foo2)
True
>>> references_sth_out_of_scope(foo3)
False
>>> references_sth_out_of_scope(references_sth_out_of_scope) # :)
True
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With