I'm trying to find out if it's possible to resolve variables in stack frames (as returned by inspect.currentframe()).
In other words, I'm looking for a function
def resolve_variable(variable_name, frame_object):
return value_of_that_variable_in_that_stackframe
For an example, consider the following piece of code:
global_var = 'global'
def foo():
closure_var = 'closure'
def bar(param):
local_var = 'local'
frame = inspect.currentframe()
assert resolve_variable('local_var', frame) == local_var
assert resolve_variable('param', frame) == param
assert resolve_variable('closure_var', frame) == closure_var
assert resolve_variable('global_var', frame) == global_var
bar('parameter')
foo()
Local and global variables are trivially looked up through the f_locals and f_globals attributes of the frame object:
def resolve_variable(variable_name, frame_object):
try:
return frame_object.f_locals[variable_name]
except KeyError:
try:
return frame_object.f_globals[variable_name]
except KeyError:
raise NameError(varname) from None
But the problem are closure variables. They aren't stored in a dictionary like the local and global variables, as far as I know. To make things even worse, variables only become closure variables if the function actually accesses them (for example by reading its value like _ = closure_var or writing to it with nonlocal closure_var; closure_var = _). So there are actually 3 different cases:
global_var = 'global'
def foo():
unused_cvar = 'unused' # actually not a closure variable at all
readonly_cvar = 'closure'
nonlocal_cvar = 'nonlocal'
def bar(param):
nonlocal nonlocal_cvar
local_var = 'local'
_ = readonly_cvar
nonlocal_cvar = 'nonlocal'
frame = inspect.currentframe()
assert resolve_variable('local_var', frame) == local_var
assert resolve_variable('param', frame) == param
assert resolve_variable('unused_cvar', frame) == 'unused'
assert resolve_variable('readonly_cvar', frame) == readonly_cvar
assert resolve_variable('nonlocal_cvar', frame) == nonlocal_cvar
assert resolve_variable('global_var', frame) == global_var
bar('parameter')
foo()
How can I rewrite my resolve_variable function to support all of these? Is it even possible?
1 Answer 1
Not generally possible. Python only holds onto closure variables that closures actually refer to.
>>> import inspect
>>> class Demo(object):
... def __del__(self):
... print("Too late, it's gone.")
...
>>> def f():
... a = Demo()
... def g():
... return inspect.currentframe()
... return g
...
>>> frame = f()()
Too late, it's gone.
As you can see from this example, there's no hope of inspecting a from the frame frame. It's gone.
As for closure variables the frame actually used, those usually show up in f_locals. I know of one weird case where they won't, which is if the frame is for a class statement with closure variables:
>>> def f():
... a = 1
... class Foo(object):
... print(a)
... print(inspect.currentframe().f_locals)
... return Foo
...
>>> f()
1
{'__module__': '__main__', '__qualname__': 'f.<locals>.Foo'}
<class '__main__.f.<locals>.Foo'>
After digging through the CPython implementation (specifically frame objects, the LOAD_CLASSDEREF opcode, and inspect.getclosurevars), I think the only way to access class frame closure variables is going to be with ctypes, gc.get_referents, or similarly nasty means.
Also, note that the f_locals dict may not be up to date if the closure variable values have changed since it was accessed; accessing frame.f_locals again refreshes the contents, but it might be out of date again by the time you look.
7 Comments
f_locals, which confusingly, is not just locals.f_locals is really a faked useless thing. You can distinguish real locals from captured nonlocals by looking at f_code.co_varnames, co_cellvars, and co_freevars for the names of uncaptured locals, locals captured by an inner function, and closure variables handed down by an outer function. These also give you the indices into the real locals array on the frame (which has cell objects for cellvars and freevars), but you can't do much with that without the C API.f_locals for closure variables.Explore related questions
See similar questions with these tags.
__closure__and change the captured values.nonlocal closure_varcapture it even if you never reference or assign to it? It did in 3.0; I don’t know if I’ve ever checked since then...barfrom within the same function it was defined in, you can cheat and look atf_back.f.localsto see all the locals offoo, captured or not. I don’t know if that’ll help for your real use case, if you have one, but it’ll work here.