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?
A stack frame represents a single function call. You can visualize functions that call one another as virtual frames stacking on top of one another. The stack data structure is actually used for this! When one function call returns its data to its caller, then its stack frame is removed from the stack.
When a function is called in Python, a stack frame is allocated to handle the local variables of the function. When the function returns, the return value is left on top of the stack for the calling function to access. Figure 6 illustrates the call stack after the return statement on line 4.
A stack frame consists of many bytes, each of which has its own address; each kind of computer has a convention for choosing one byte whose address serves as the address of the frame. Usually this address is kept in a register called the frame pointer register while execution is going on in that frame.
In the case of a call stack, a stack frame would represent a function call and its argument data. If I remember correctly, the function return address is pushed onto the stack first, then the arguments and space for local variables. Together, they make the "frame," although this is likely architecture-dependent.
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.
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