Checking if a Python function referenced anything out-of-scope

Viewed 252

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?

2 Answers

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

Check the builtin locals() function. This will return a dictionary of the variables in the local scope.

global_var = "Hello"

def foo():
 local_var = "world"
 if "local_var" in locals(): print(c)
 if "global_var" in locals(): print(global_var)

>>> foo()
world

Related