Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Resolve a variable name given only a stack frame object

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?

like image 363
Aran-Fey Avatar asked Apr 04 '18 23:04

Aran-Fey


People also ask

What is a stack frame in Python?

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.

Does Python use stack frame?

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.

What is contained in a stack frame?

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.

What is found in the stack frame of a called function?

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.


1 Answers

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.

like image 169
user2357112 supports Monica Avatar answered Nov 03 '22 20:11

user2357112 supports Monica